From 3f2462f7dfde46beff4cf15f4f20db1f6308e19b Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Mon, 23 Mar 2020 16:53:54 +0000 Subject: [PATCH 01/48] Add NaCl's symmetric key authenticated decryption code Copied relevant bits for our decryption needs from: https://github.com/freeeve/nacl-java/blob/0369ed1b166591db9f7a2d511a3554c2376871dd/src/main/java/com/caligochat/nacl/SecretBox.java adjusting the entry point to take the key in the constructor, have preconditions checked and better signalling AuthenticityException, and added the clearKey method. Added MIT License to the copied files. --- .../crypto/nacl/AuthenticityException.java | 27 + .../pusher/client/crypto/nacl/Poly1305.java | 1559 +++++++++++++++++ .../com/pusher/client/crypto/nacl/Salsa.java | 433 +++++ .../client/crypto/nacl/SecretBoxOpener.java | 137 ++ .../com/pusher/client/crypto/nacl/Subtle.java | 45 + 5 files changed, 2201 insertions(+) create mode 100644 src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java create mode 100644 src/main/java/com/pusher/client/crypto/nacl/Poly1305.java create mode 100644 src/main/java/com/pusher/client/crypto/nacl/Salsa.java create mode 100644 src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java create mode 100644 src/main/java/com/pusher/client/crypto/nacl/Subtle.java diff --git a/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java b/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java new file mode 100644 index 00000000..18150119 --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java @@ -0,0 +1,27 @@ +/* +Copyright 2015 Eve Freeman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +package com.pusher.client.crypto.nacl; + +public class AuthenticityException extends RuntimeException { +} diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java new file mode 100644 index 00000000..307ebcf7 --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -0,0 +1,1559 @@ +/* +Copyright 2015 Eve Freeman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +package com.pusher.client.crypto.nacl; + +public class Poly1305 { + public static int TAG_SIZE = 16; + + private static double alpham80 = 0.00000000558793544769287109375d; + private static double alpham48 = 24.0d; + private static double alpham16 = 103079215104.0d; + private static double alpha0 = 6755399441055744.0d; + private static double alpha18 = 1770887431076116955136.0d; + private static double alpha32 = 29014219670751100192948224.0d; + private static double alpha50 = 7605903601369376408980219232256.0d; + private static double alpha64 = 124615124604835863084731911901282304.0d; + private static double alpha82 = 32667107224410092492483962313449748299776.0d; + private static double alpha96 = 535217884764734955396857238543560676143529984.0d; + private static double alpha112 = 35076039295941670036888435985190792471742381031424.0d; + private static double alpha130 = 9194973245195333150150082162901855101712434733101613056.0d; + private static double scale = 0.0000000000000000000000000000000000000036734198463196484624023016788195177431833298649127735047148490821200539357960224151611328125d; + private static double offset0 = 6755408030990331.0d; + private static double offset1 = 29014256564239239022116864.0d; + private static double offset2 = 124615283061160854719918951570079744.0d; + private static double offset3 = 535219245894202480694386063513315216128475136.0d; + + private static long uint32(long x) { + return 0xFFFFFFFF & x; + } + + private static double longBitsToDouble(long bits) { + int s = ((bits >> 63) == 0) ? 1 : -1; + int e = (int) ((bits >> 52) & 0x7ffL); + long m = (e == 0) ? + (bits & 0xfffffffffffffL) << 1 : + (bits & 0xfffffffffffffL) | 0x10000000000000L; + return (double) s * (double) m * Math.pow(2, e - 1075); + } + + public static boolean verify(byte mac[], byte m[], byte key[]) { + byte tmp[] = sum(m, key); + //Util.printHex("tmp", tmp); + //Util.printHex("mac", mac); + return Subtle.constantTimeCompare(tmp, mac); + } + + // Sum generates an authenticator for m using a one-time key and puts the + // 16-byte result into out. Authenticating two different messages with the same + // key allows an attacker to forge messages at will. + public static byte[] sum(byte m[], byte key[]) { + byte r[] = key.clone(); + byte s[] = new byte[16]; + for (int i = 0; i < s.length; i++) { + s[i] = key[i + 16]; + } + + double y7; + double y6; + double y1; + double y0; + double y5; + double y4; + double x7; + double x6; + double x1; + double x0; + double y3; + double y2; + double x5; + double r3lowx0; + double x4; + double r0lowx6; + double x3; + double r3highx0; + double x2; + double r0highx6; + double r0lowx0; + double sr1lowx6; + double r0highx0; + double sr1highx6; + double sr3low; + double r1lowx0; + double sr2lowx6; + double r1highx0; + double sr2highx6; + double r2lowx0; + double sr3lowx6; + double r2highx0; + double sr3highx6; + double r1highx4; + double r1lowx4; + double r0highx4; + double r0lowx4; + double sr3highx4; + double sr3lowx4; + double sr2highx4; + double sr2lowx4; + double r0lowx2; + double r0highx2; + double r1lowx2; + double r1highx2; + double r2lowx2; + double r2highx2; + double sr3lowx2; + double sr3highx2; + double z0; + double z1; + double z2; + double z3; + long m0; + long m1; + long m2; + long m3; + long m00;//uint32 + long m01;//uint32 + long m02;//uint32 + long m03;//uint32 + long m10;//uint32 + long m11;//uint32 + long m12;//uint32 + long m13;//uint32 + long m20;//uint32 + long m21;//uint32 + long m22;//uint32 + long m23;//uint32 + long m30;//uint32 + long m31;//uint32 + long m32;//uint32 + long m33;//uint64 + long lbelow2;//int32 + long lbelow3;//int32 + long lbelow4;//int32 + long lbelow5;//int32 + long lbelow6;//int32 + long lbelow7;//int32 + long lbelow8;//int32 + long lbelow9;//int32 + long lbelow10;//int32 + long lbelow11;//int32 + long lbelow12;//int32 + long lbelow13;//int32 + long lbelow14;//int32 + long lbelow15;//int32 + long s00;//uint32 + long s01;//uint32 + long s02;//uint32 + long s03;//uint32 + long s10;//uint32 + long s11;//uint32 + long s12;//uint32 + long s13;//uint32 + long s20;//uint32 + long s21;//uint32 + long s22;//uint32 + long s23;//uint32 + long s30;//uint32 + long s31;//uint32 + long s32;//uint32 + long s33;//uint32 + long bits32;//uint64 + long f;//uint64 + long f0;//uint64 + long f1;//uint64 + long f2;//uint64 + long f3;//uint64 + long f4;//uint64 + long g;//uint64 + long g0;//uint64 + long g1;//uint64 + long g2;//uint64 + long g3;//uint64 + long g4;//uint64 + + long p = 0; + + int l = m.length; + + long r00 = 0xFF & r[0]; + long r01 = 0xFF & r[1]; + long r02 = 0xFF & r[2]; + long r0 = 2151; + + long r03 = 0xFF & r[3]; + r03 &= 15; + r0 <<= 51; + + long r10 = 0xFF & r[4]; + r10 &= 252; + r01 <<= 8; + r0 += r00; + + long r11 = 0xFF & r[5]; + r02 <<= 16; + r0 += r01; + + long r12 = 0xFF & r[6]; + r03 <<= 24; + r0 += r02; + + long r13 = 0xFF & r[7]; + r13 &= 15; + long r1 = 2215; + r0 += r03; + + long d0 = r0; + r1 <<= 51; + long r2 = 2279; + + long r20 = 0xFF & r[8]; + r20 &= 252; + r11 <<= 8; + r1 += r10; + + long r21 = 0xFF & r[9]; + r12 <<= 16; + r1 += r11; + + long r22 = 0xFF & r[10]; + r13 <<= 24; + r1 += r12; + + long r23 = 0xFF & r[11]; + r23 &= 15; + r2 <<= 51; + r1 += r13; + + long d1 = r1; + r21 <<= 8; + r2 += r20; + + long r30 = 0xFF & r[12]; + r30 &= 252; + r22 <<= 16; + r2 += r21; + + long r31 = 0xFF & r[13]; + r23 <<= 24; + r2 += r22; + + long r32 = 0xFF & r[14]; + r2 += r23; + long r3 = 2343; + + long d2 = r2; + r3 <<= 51; + + long r33 = 0xFF & r[15]; + r33 &= 15; + r31 <<= 8; + r3 += r30; + + r32 <<= 16; + r3 += r31; + + r33 <<= 24; + r3 += r32; + + r3 += r33; + double h0 = alpha32 - alpha32; + + long d3 = r3; + double h1 = alpha32 - alpha32; + + double h2 = alpha32 - alpha32; + + double h3 = alpha32 - alpha32; + + double h4 = alpha32 - alpha32; + + double r0low = Double.longBitsToDouble(d0); + double h5 = alpha32 - alpha32; + + double r1low = longBitsToDouble(d1); + double h6 = alpha32 - alpha32; + + double r2low = Double.longBitsToDouble(d2); + double h7 = alpha32 - alpha32; + + r0low -= alpha0; + + r1low -= alpha32; + + r2low -= alpha64; + + double r0high = r0low + alpha18; + + double r3low = Double.longBitsToDouble(d3); + + double r1high = r1low + alpha50; + double sr1low = scale * r1low; + + double r2high = r2low + alpha82; + double sr2low = scale * r2low; + + r0high -= alpha18; + double r0high_stack = r0high; + + r3low -= alpha96; + + r1high -= alpha50; + double r1high_stack = r1high; + + double sr1high = sr1low + alpham80; + + r0low -= r0high; + + r2high -= alpha82; + sr3low = scale * r3low; + + double sr2high = sr2low + alpham48; + + r1low -= r1high; + double r1low_stack = r1low; + + sr1high -= alpham80; + double sr1high_stack = sr1high; + + r2low -= r2high; + double r2low_stack = r2low; + + sr2high -= alpham48; + double sr2high_stack = sr2high; + + double r3high = r3low + alpha112; + double r0low_stack = r0low; + + sr1low -= sr1high; + double sr1low_stack = sr1low; + + double sr3high = sr3low + alpham16; + double r2high_stack = r2high; + + sr2low -= sr2high; + double sr2low_stack = sr2low; + + r3high -= alpha112; + double r3high_stack = r3high; + + sr3high -= alpham16; + double sr3high_stack = sr3high; + + r3low -= r3high; + double r3low_stack = r3low; + + sr3low -= sr3high; + double sr3low_stack = sr3low; + + + if (!(l < 16)) { + m00 = 0xFF & m[(int) p]; + m0 = 2151; + + m0 <<= 51; + m1 = 2215; + m01 = 0xFF & m[(int) p + 1]; + + m1 <<= 51; + m2 = 2279; + m02 = 0xFF & m[(int) p + 2]; + + m2 <<= 51; + m3 = 2343; + m03 = 0xFF & (m[(int) p + 3]); + + m10 = 0xFF & (m[(int) p + 4]); + m01 <<= 8; + m0 += m00; + + m11 = 0xFF & (m[(int) p + 5]); + m02 <<= 16; + m0 += m01; + + m12 = 0xFF & (m[(int) p + 6]); + m03 <<= 24; + m0 += m02; + + m13 = 0xFF & (m[(int) p + 7]); + m3 <<= 51; + m0 += m03; + + m20 = 0xFF & (m[(int) p + 8]); + m11 <<= 8; + m1 += m10; + + m21 = 0xFF & (m[(int) p + 9]); + m12 <<= 16; + m1 += m11; + + m22 = 0xFF & (m[(int) p + 10]); + m13 <<= 24; + m1 += m12; + + m23 = 0xFF & (m[(int) p + 11]); + m1 += m13; + + m30 = 0xFF & (m[(int) p + 12]); + m21 <<= 8; + m2 += m20; + + m31 = 0xFF & (m[(int) p + 13]); + m22 <<= 16; + m2 += m21; + + m32 = 0xFF & (m[(int) p + 14]); + m23 <<= 24; + m2 += m22; + + m33 = 0xFF & (m[(int) p + 15]); + m2 += m23; + + d0 = m0; + m31 <<= 8; + m3 += m30; + + d1 = m1; + m32 <<= 16; + m3 += m31; + + d2 = m2; + m33 += 256; + + m33 <<= 24; + m3 += m32; + + m3 += m33; + d3 = m3; + + p += 16; + l -= 16; + + z0 = Double.longBitsToDouble(d0); + + z1 = Double.longBitsToDouble(d1); + + z2 = Double.longBitsToDouble(d2); + + z3 = Double.longBitsToDouble(d3); + + z0 -= alpha0; + + z1 -= alpha32; + + z2 -= alpha64; + + z3 -= alpha96; + + h0 += z0; + + h1 += z1; + + h3 += z2; + + h5 += z3; + + while (l >= 16) { + //multiplyaddatleast16bytes: + + m2 = 2279; + m20 = 0xFF & (m[(int) p + 8]); + y7 = h7 + alpha130; + + m2 <<= 51; + m3 = 2343; + m21 = 0xFF & (m[(int) p + 9]); + y6 = h6 + alpha130; + + m3 <<= 51; + m0 = 2151; + m22 = 0xFF & (m[(int) p + 10]); + y1 = h1 + alpha32; + + m0 <<= 51; + m1 = 2215; + m23 = 0xFF & (m[(int) p + 11]); + y0 = h0 + alpha32; + + m1 <<= 51; + m30 = 0xFF & (m[(int) p + 12]); + y7 -= alpha130; + + m21 <<= 8; + m2 += m20; + m31 = 0xFF & (m[(int) p + 13]); + y6 -= alpha130; + + m22 <<= 16; + m2 += m21; + m32 = 0xFF & (m[(int) p + 14]); + y1 -= alpha32; + + m23 <<= 24; + m2 += m22; + m33 = 0xFF & (m[(int) p + 15]); + y0 -= alpha32; + + m2 += m23; + m00 = 0xFF & (m[(int) p + 0]); + y5 = h5 + alpha96; + + m31 <<= 8; + m3 += m30; + m01 = 0xFF & (m[(int) p + 1]); + y4 = h4 + alpha96; + + m32 <<= 16; + m02 = 0xFF & (m[(int) p + 2]); + x7 = h7 - y7; + y7 *= scale; + + m33 += 256; + m03 = 0xFF & (m[(int) p + 3]); + x6 = h6 - y6; + y6 *= scale; + + m33 <<= 24; + m3 += m31; + m10 = 0xFF & (m[(int) p + 4]); + x1 = h1 - y1; + + m01 <<= 8; + m3 += m32; + m11 = 0xFF & (m[(int) p + 5]); + x0 = h0 - y0; + + m3 += m33; + m0 += m00; + m12 = 0xFF & (m[(int) p + 6]); + y5 -= alpha96; + + m02 <<= 16; + m0 += m01; + m13 = 0xFF & (m[(int) p + 7]); + y4 -= alpha96; + + m03 <<= 24; + m0 += m02; + d2 = m2; + x1 += y7; + + m0 += m03; + d3 = m3; + x0 += y6; + + m11 <<= 8; + m1 += m10; + d0 = m0; + x7 += y5; + + m12 <<= 16; + m1 += m11; + x6 += y4; + + m13 <<= 24; + m1 += m12; + y3 = h3 + alpha64; + + m1 += m13; + d1 = m1; + y2 = h2 + alpha64; + + x0 += x1; + + x6 += x7; + + y3 -= alpha64; + r3low = r3low_stack; + + y2 -= alpha64; + r0low = r0low_stack; + + x5 = h5 - y5; + r3lowx0 = r3low * x0; + r3high = r3high_stack; + + x4 = h4 - y4; + r0lowx6 = r0low * x6; + r0high = r0high_stack; + + x3 = h3 - y3; + r3highx0 = r3high * x0; + sr1low = sr1low_stack; + + x2 = h2 - y2; + r0highx6 = r0high * x6; + sr1high = sr1high_stack; + + x5 += y3; + r0lowx0 = r0low * x0; + r1low = r1low_stack; + + h6 = r3lowx0 + r0lowx6; + sr1lowx6 = sr1low * x6; + r1high = r1high_stack; + + x4 += y2; + r0highx0 = r0high * x0; + sr2low = sr2low_stack; + + h7 = r3highx0 + r0highx6; + sr1highx6 = sr1high * x6; + sr2high = sr2high_stack; + + x3 += y1; + r1lowx0 = r1low * x0; + r2low = r2low_stack; + + h0 = r0lowx0 + sr1lowx6; + sr2lowx6 = sr2low * x6; + r2high = r2high_stack; + + x2 += y0; + r1highx0 = r1high * x0; + sr3low = sr3low_stack; + + h1 = r0highx0 + sr1highx6; + sr2highx6 = sr2high * x6; + sr3high = sr3high_stack; + + x4 += x5; + r2lowx0 = r2low * x0; + z2 = Double.longBitsToDouble(d2); + + h2 = r1lowx0 + sr2lowx6; + sr3lowx6 = sr3low * x6; + + x2 += x3; + r2highx0 = r2high * x0; + z3 = Double.longBitsToDouble(d3); + + h3 = r1highx0 + sr2highx6; + sr3highx6 = sr3high * x6; + + r1highx4 = r1high * x4; + z2 -= alpha64; + + h4 = r2lowx0 + sr3lowx6; + r1lowx4 = r1low * x4; + + r0highx4 = r0high * x4; + z3 -= alpha96; + + h5 = r2highx0 + sr3highx6; + r0lowx4 = r0low * x4; + + h7 += r1highx4; + sr3highx4 = sr3high * x4; + + h6 += r1lowx4; + sr3lowx4 = sr3low * x4; + + h5 += r0highx4; + sr2highx4 = sr2high * x4; + + h4 += r0lowx4; + sr2lowx4 = sr2low * x4; + + h3 += sr3highx4; + r0lowx2 = r0low * x2; + + h2 += sr3lowx4; + r0highx2 = r0high * x2; + + h1 += sr2highx4; + r1lowx2 = r1low * x2; + + h0 += sr2lowx4; + r1highx2 = r1high * x2; + + h2 += r0lowx2; + r2lowx2 = r2low * x2; + + h3 += r0highx2; + r2highx2 = r2high * x2; + + h4 += r1lowx2; + sr3lowx2 = sr3low * x2; + + h5 += r1highx2; + sr3highx2 = sr3high * x2; + + p += 16; + l -= 16; + h6 += r2lowx2; + + h7 += r2highx2; + + z1 = Double.longBitsToDouble(d1); + h0 += sr3lowx2; + + z0 = Double.longBitsToDouble(d0); + h1 += sr3highx2; + + z1 -= alpha32; + + z0 -= alpha0; + + h5 += z3; + + h3 += z2; + + h1 += z1; + + h0 += z0; + + } + + // multiplyaddatmost15bytes: + y7 = h7 + alpha130; + + y6 = h6 + alpha130; + + y1 = h1 + alpha32; + + y0 = h0 + alpha32; + + y7 -= alpha130; + + y6 -= alpha130; + + y1 -= alpha32; + + y0 -= alpha32; + + y5 = h5 + alpha96; + + y4 = h4 + alpha96; + + x7 = h7 - y7; + y7 *= scale; + + x6 = h6 - y6; + y6 *= scale; + + x1 = h1 - y1; + + x0 = h0 - y0; + + y5 -= alpha96; + + y4 -= alpha96; + + x1 += y7; + + x0 += y6; + + x7 += y5; + + x6 += y4; + + y3 = h3 + alpha64; + + y2 = h2 + alpha64; + + x0 += x1; + + x6 += x7; + + y3 -= alpha64; + r3low = r3low_stack; + + y2 -= alpha64; + r0low = r0low_stack; + + x5 = h5 - y5; + r3lowx0 = r3low * x0; + r3high = r3high_stack; + + x4 = h4 - y4; + r0lowx6 = r0low * x6; + r0high = r0high_stack; + + x3 = h3 - y3; + r3highx0 = r3high * x0; + sr1low = sr1low_stack; + + x2 = h2 - y2; + r0highx6 = r0high * x6; + sr1high = sr1high_stack; + + x5 += y3; + r0lowx0 = r0low * x0; + r1low = r1low_stack; + + h6 = r3lowx0 + r0lowx6; + sr1lowx6 = sr1low * x6; + r1high = r1high_stack; + + x4 += y2; + r0highx0 = r0high * x0; + sr2low = sr2low_stack; + + h7 = r3highx0 + r0highx6; + sr1highx6 = sr1high * x6; + sr2high = sr2high_stack; + + x3 += y1; + r1lowx0 = r1low * x0; + r2low = r2low_stack; + + h0 = r0lowx0 + sr1lowx6; + sr2lowx6 = sr2low * x6; + r2high = r2high_stack; + + x2 += y0; + r1highx0 = r1high * x0; + sr3low = sr3low_stack; + + h1 = r0highx0 + sr1highx6; + sr2highx6 = sr2high * x6; + sr3high = sr3high_stack; + + x4 += x5; + r2lowx0 = r2low * x0; + + h2 = r1lowx0 + sr2lowx6; + sr3lowx6 = sr3low * x6; + + x2 += x3; + r2highx0 = r2high * x0; + + h3 = r1highx0 + sr2highx6; + sr3highx6 = sr3high * x6; + + r1highx4 = r1high * x4; + + h4 = r2lowx0 + sr3lowx6; + r1lowx4 = r1low * x4; + + r0highx4 = r0high * x4; + + h5 = r2highx0 + sr3highx6; + r0lowx4 = r0low * x4; + + h7 += r1highx4; + sr3highx4 = sr3high * x4; + + h6 += r1lowx4; + sr3lowx4 = sr3low * x4; + + h5 += r0highx4; + sr2highx4 = sr2high * x4; + + h4 += r0lowx4; + sr2lowx4 = sr2low * x4; + + h3 += sr3highx4; + r0lowx2 = r0low * x2; + + h2 += sr3lowx4; + r0highx2 = r0high * x2; + + h1 += sr2highx4; + r1lowx2 = r1low * x2; + + h0 += sr2lowx4; + r1highx2 = r1high * x2; + + h2 += r0lowx2; + r2lowx2 = r2low * x2; + + h3 += r0highx2; + r2highx2 = r2high * x2; + + h4 += r1lowx2; + sr3lowx2 = sr3low * x2; + + h5 += r1highx2; + sr3highx2 = sr3high * x2; + + h6 += r2lowx2; + + h7 += r2highx2; + + h0 += sr3lowx2; + + h1 += sr3highx2; + } + + // addatmost15bytes: + + if (l > 0) { + lbelow2 = l - 2; + + lbelow3 = l - 3; + + lbelow2 >>= 31; + lbelow4 = l - 4; + + m00 = 0xFF & (m[(int) p + 0]); + lbelow3 >>= 31; + p += lbelow2; + + m01 = 0xFF & (m[(int) p + 1]); + lbelow4 >>= 31; + p += lbelow3; + + m02 = 0xFF & (m[(int) p + 2]); + p += lbelow4; + m0 = 2151; + + m03 = 0xFF & (m[(int) p + 3]); + m0 <<= 51; + m1 = 2215; + + m0 += m00; + m01 &= ~uint32(lbelow2); + + m02 &= ~uint32(lbelow3); + m01 -= uint32(lbelow2); + + m01 <<= 8; + m03 &= ~uint32(lbelow4); + + m0 += m01; + lbelow2 -= lbelow3; + + m02 += uint32(lbelow2); + lbelow3 -= lbelow4; + + m02 <<= 16; + m03 += uint32(lbelow3); + + m03 <<= 24; + m0 += m02; + + m0 += m03; + lbelow5 = l - 5; + + lbelow6 = l - 6; + lbelow7 = l - 7; + + lbelow5 >>= 31; + lbelow8 = l - 8; + + lbelow6 >>= 31; + p += lbelow5; + + m10 = 0xFF & (m[(int) p + 4]); + lbelow7 >>= 31; + p += lbelow6; + + m11 = 0xFF & (m[(int) p + 5]); + lbelow8 >>= 31; + p += lbelow7; + + m12 = 0xFF & (m[(int) p + 6]); + m1 <<= 51; + p += lbelow8; + + m13 = 0xFF & (m[(int) p + 7]); + m10 &= ~uint32(lbelow5); + lbelow4 -= lbelow5; + + m10 += uint32(lbelow4); + lbelow5 -= lbelow6; + + m11 &= ~uint32(lbelow6); + m11 += uint32(lbelow5); + + m11 <<= 8; + m1 += m10; + + m1 += m11; + m12 &= ~uint32(lbelow7); + + lbelow6 -= lbelow7; + m13 &= ~uint32(lbelow8); + + m12 += uint32(lbelow6); + lbelow7 -= lbelow8; + + m12 <<= 16; + m13 += uint32(lbelow7); + + m13 <<= 24; + m1 += m12; + + m1 += m13; + m2 = 2279; + + lbelow9 = l - 9; + m3 = 2343; + + lbelow10 = l - 10; + lbelow11 = l - 11; + + lbelow9 >>= 31; + lbelow12 = l - 12; + + lbelow10 >>= 31; + p += lbelow9; + + m20 = 0xFF & (m[(int) p + 8]); + lbelow11 >>= 31; + p += lbelow10; + + m21 = 0xFF & (m[(int) p + 9]); + lbelow12 >>= 31; + p += lbelow11; + + m22 = 0xFF & (m[(int) p + 10]); + m2 <<= 51; + p += lbelow12; + + m23 = 0xFF & (m[(int) p + 11]); + m20 &= ~uint32(lbelow9); + lbelow8 -= lbelow9; + + m20 += uint32(lbelow8); + lbelow9 -= lbelow10; + + m21 &= ~uint32(lbelow10); + m21 += uint32(lbelow9); + + m21 <<= 8; + m2 += m20; + + m2 += m21; + m22 &= ~uint32(lbelow11); + + lbelow10 -= lbelow11; + m23 &= ~uint32(lbelow12); + + m22 += uint32(lbelow10); + lbelow11 -= lbelow12; + + m22 <<= 16; + m23 += uint32(lbelow11); + + m23 <<= 24; + m2 += m22; + + m3 <<= 51; + lbelow13 = l - 13; + + lbelow13 >>= 31; + lbelow14 = l - 14; + + lbelow14 >>= 31; + p += lbelow13; + lbelow15 = l - 15; + + m30 = uint32(m[(int) p + 12]); + lbelow15 >>= 31; + p += lbelow14; + + m31 = 0xFF & (m[(int) p + 13]); + p += lbelow15; + m2 += m23; + + m32 = 0xFF & (m[(int) p + 14]); + m30 &= ~uint32(lbelow13); + lbelow12 -= lbelow13; + + m30 += uint32(lbelow12); + lbelow13 -= lbelow14; + + m3 += m30; + m31 &= ~uint32(lbelow14); + + m31 += uint32(lbelow13); + m32 &= ~uint32(lbelow15); + + m31 <<= 8; + lbelow14 -= lbelow15; + + m3 += m31; + m32 += uint32(lbelow14); + d0 = m0; + + m32 <<= 16; + m33 = lbelow15 + 1; + d1 = m1; + + m33 <<= 24; + m3 += m32; + d2 = m2; + + m3 += m33; + d3 = m3; + + z3 = Double.longBitsToDouble(d3); + ; + + z2 = Double.longBitsToDouble(d2); + + z1 = Double.longBitsToDouble(d1); + + z0 = Double.longBitsToDouble(d0); + + z3 -= alpha96; + + z2 -= alpha64; + + z1 -= alpha32; + + z0 -= alpha0; + + h5 += z3; + + h3 += z2; + + h1 += z1; + + h0 += z0; + + y7 = h7 + alpha130; + + y6 = h6 + alpha130; + + y1 = h1 + alpha32; + + y0 = h0 + alpha32; + + y7 -= alpha130; + + y6 -= alpha130; + + y1 -= alpha32; + + y0 -= alpha32; + + y5 = h5 + alpha96; + + y4 = h4 + alpha96; + + x7 = h7 - y7; + y7 *= scale; + + x6 = h6 - y6; + y6 *= scale; + + x1 = h1 - y1; + + x0 = h0 - y0; + + y5 -= alpha96; + + y4 -= alpha96; + + x1 += y7; + + x0 += y6; + + x7 += y5; + + x6 += y4; + + y3 = h3 + alpha64; + + y2 = h2 + alpha64; + + x0 += x1; + + x6 += x7; + + y3 -= alpha64; + r3low = r3low_stack; + + y2 -= alpha64; + r0low = r0low_stack; + + x5 = h5 - y5; + r3lowx0 = r3low * x0; + r3high = r3high_stack; + + x4 = h4 - y4; + r0lowx6 = r0low * x6; + r0high = r0high_stack; + + x3 = h3 - y3; + r3highx0 = r3high * x0; + sr1low = sr1low_stack; + + x2 = h2 - y2; + r0highx6 = r0high * x6; + sr1high = sr1high_stack; + + x5 += y3; + r0lowx0 = r0low * x0; + r1low = r1low_stack; + + h6 = r3lowx0 + r0lowx6; + sr1lowx6 = sr1low * x6; + r1high = r1high_stack; + + x4 += y2; + r0highx0 = r0high * x0; + sr2low = sr2low_stack; + + h7 = r3highx0 + r0highx6; + sr1highx6 = sr1high * x6; + sr2high = sr2high_stack; + + x3 += y1; + r1lowx0 = r1low * x0; + r2low = r2low_stack; + + h0 = r0lowx0 + sr1lowx6; + sr2lowx6 = sr2low * x6; + r2high = r2high_stack; + + x2 += y0; + r1highx0 = r1high * x0; + sr3low = sr3low_stack; + + h1 = r0highx0 + sr1highx6; + sr2highx6 = sr2high * x6; + sr3high = sr3high_stack; + + x4 += x5; + r2lowx0 = r2low * x0; + + h2 = r1lowx0 + sr2lowx6; + sr3lowx6 = sr3low * x6; + + x2 += x3; + r2highx0 = r2high * x0; + + h3 = r1highx0 + sr2highx6; + sr3highx6 = sr3high * x6; + + r1highx4 = r1high * x4; + + h4 = r2lowx0 + sr3lowx6; + r1lowx4 = r1low * x4; + + r0highx4 = r0high * x4; + + h5 = r2highx0 + sr3highx6; + r0lowx4 = r0low * x4; + + h7 += r1highx4; + sr3highx4 = sr3high * x4; + + h6 += r1lowx4; + sr3lowx4 = sr3low * x4; + + h5 += r0highx4; + sr2highx4 = sr2high * x4; + + h4 += r0lowx4; + sr2lowx4 = sr2low * x4; + + h3 += sr3highx4; + r0lowx2 = r0low * x2; + + h2 += sr3lowx4; + r0highx2 = r0high * x2; + + h1 += sr2highx4; + r1lowx2 = r1low * x2; + + h0 += sr2lowx4; + r1highx2 = r1high * x2; + + h2 += r0lowx2; + r2lowx2 = r2low * x2; + + h3 += r0highx2; + r2highx2 = r2high * x2; + + h4 += r1lowx2; + sr3lowx2 = sr3low * x2; + + h5 += r1highx2; + sr3highx2 = sr3high * x2; + + h6 += r2lowx2; + + h7 += r2highx2; + + h0 += sr3lowx2; + + h1 += sr3highx2; + } + + //nomorebytes: + + y7 = h7 + alpha130; + + y0 = h0 + alpha32; + + y1 = h1 + alpha32; + + y2 = h2 + alpha64; + + y7 -= alpha130; + + y3 = h3 + alpha64; + + y4 = h4 + alpha96; + + y5 = h5 + alpha96; + + x7 = h7 - y7; + y7 *= scale; + + y0 -= alpha32; + + y1 -= alpha32; + + y2 -= alpha64; + + h6 += x7; + + y3 -= alpha64; + + y4 -= alpha96; + + y5 -= alpha96; + + y6 = h6 + alpha130; + + x0 = h0 - y0; + + x1 = h1 - y1; + + x2 = h2 - y2; + + y6 -= alpha130; + + x0 += y7; + + x3 = h3 - y3; + + x4 = h4 - y4; + + x5 = h5 - y5; + + x6 = h6 - y6; + + y6 *= scale; + + x2 += y0; + + x3 += y1; + + x4 += y2; + + x0 += y6; + + x5 += y3; + + x6 += y4; + + x2 += x3; + + x0 += x1; + + x4 += x5; + + x6 += y5; + + x2 += offset1; + d1 = Double.doubleToLongBits(x2); + + x0 += offset0; + d0 = Double.doubleToLongBits(x0); + + x4 += offset2; + d2 = Double.doubleToLongBits(x4); + + x6 += offset3; + d3 = Double.doubleToLongBits(x6); + + f0 = d0; + + f1 = d1; + bits32 = 0xFFFFFFFFFFFFFFFFl; + + f2 = d2; + bits32 >>>= 32; + + f3 = d3; + f = f0 >> 32; + + f0 &= bits32; + f &= 255; + + f1 += f; + g0 = f0 + 5; + + g = g0 >> 32; + g0 &= bits32; + + f = f1 >> 32; + f1 &= bits32; + + f &= 255; + g1 = f1 + g; + + g = g1 >> 32; + f2 += f; + + f = f2 >> 32; + g1 &= bits32; + + f2 &= bits32; + f &= 255; + + f3 += f; + g2 = f2 + g; + + g = g2 >> 32; + g2 &= bits32; + + f4 = f3 >> 32; + f3 &= bits32; + + f4 &= 255; + g3 = f3 + g; + + g = g3 >> 32; + g3 &= bits32; + + g4 = f4 + g; + + g4 = g4 - 4; + s00 = 0xFF & (s[0]); + + f = g4 >> 63; + s01 = 0xFF & (s[1]); + + f0 &= f; + g0 &= ~f; + s02 = 0xFF & (s[2]); + + f1 &= f; + f0 |= g0; + s03 = 0xFF & (s[3]); + + g1 &= ~f; + f2 &= f; + s10 = 0xFF & (s[4]); + + f3 &= f; + g2 &= ~f; + s11 = 0xFF & (s[5]); + + g3 &= ~f; + f1 |= g1; + s12 = 0xFF & (s[6]); + + f2 |= g2; + f3 |= g3; + s13 = 0xFF & (s[7]); + + s01 <<= 8; + f0 += s00; + s20 = 0xFF & (s[8]); + + s02 <<= 16; + f0 += s01; + s21 = 0xFF & (s[9]); + + s03 <<= 24; + f0 += s02; + s22 = 0xFF & (s[10]); + + s11 <<= 8; + f1 += s10; + s23 = 0xFF & (s[11]); + + s12 <<= 16; + f1 += s11; + s30 = 0xFF & (s[12]); + + s13 <<= 24; + f1 += (s12); + s31 = 0xFF & s[13]; + + f0 += (s03); + f1 += (s13); + s32 = 0xFF & (s[14]); + + s21 <<= 8; + f2 += (s20); + s33 = 0xFF & (s[15]); + + s22 <<= 16; + f2 += (s21); + + s23 <<= 24; + f2 += (s22); + + s31 <<= 8; + f3 += (s30); + + s32 <<= 16; + f3 += (s31); + + s33 <<= 24; + f3 += (s32); + + f2 += (s23); + f3 += s33; + + byte out[] = new byte[16]; + out[0] = (byte) (f0); + f0 >>= 8; + out[1] = (byte) (f0); + f0 >>= 8; + out[2] = (byte) (f0); + f0 >>= 8; + out[3] = (byte) (f0); + f0 >>= 8; + f1 += f0; + + out[4] = (byte) (f1); + f1 >>= 8; + out[5] = (byte) (f1); + f1 >>= 8; + out[6] = (byte) (f1); + f1 >>= 8; + out[7] = (byte) (f1); + f1 >>= 8; + f2 += f1; + + out[8] = (byte) (f2); + f2 >>= 8; + out[9] = (byte) (f2); + f2 >>= 8; + out[10] = (byte) (f2); + f2 >>= 8; + out[11] = (byte) (f2); + f2 >>= 8; + f3 += f2; + + out[12] = (byte) f3; + f3 >>= 8; + out[13] = (byte) f3; + f3 >>= 8; + out[14] = (byte) f3; + f3 >>= 8; + out[15] = (byte) f3; + return out; + } +} diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java new file mode 100644 index 00000000..a95e214c --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java @@ -0,0 +1,433 @@ +/* +Copyright 2015 Eve Freeman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +package com.pusher.client.crypto.nacl; + +public class Salsa { + public static byte[] SIGMA = {'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'}; + + private static int rounds = 20; + + private static long mask(byte x) { + return 0xFFl & x; + } + + // core applies the Salsa20 core function to 16-byte input in, 32-byte key k, + // and 16-byte constant c, and puts the result into 64-byte array out. + public static byte[] core(byte in[], byte k[], byte c[]) { + byte out[] = new byte[64]; + long mask = 0xFFFFFFFFl; + + long j0 = mask & (mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24); + long j1 = mask & (mask(k[0]) | mask(k[1]) << 8 | mask(k[2]) << 16 | mask(k[3]) << 24); + long j2 = mask & (mask(k[4]) | mask(k[5]) << 8 | mask(k[6]) << 16 | mask(k[7]) << 24); + long j3 = mask & (mask(k[8]) | mask(k[9]) << 8 | mask(k[10]) << 16 | mask(k[11]) << 24); + long j4 = mask & (mask(k[12]) | mask(k[13]) << 8 | mask(k[14]) << 16 | mask(k[15]) << 24); + long j5 = mask & (mask(c[4]) | mask(c[5]) << 8 | mask(c[6]) << 16 | mask(c[7]) << 24); + long j6 = mask & (mask(in[0]) | mask(in[1]) << 8 | mask(in[2]) << 16 | mask(in[3]) << 24); + long j7 = mask & (mask(in[4]) | mask(in[5]) << 8 | mask(in[6]) << 16 | mask(in[7]) << 24); + long j8 = mask & (mask(in[8]) | mask(in[9]) << 8 | mask(in[10]) << 16 | mask(in[11]) << 24); + long j9 = mask & (mask(in[12]) | mask(in[13]) << 8 | mask(in[14]) << 16 | mask(in[15]) << 24); + long j10 = mask & (mask(c[8]) | mask(c[9]) << 8 | mask(c[10]) << 16 | mask(c[11]) << 24); + long j11 = mask & (mask(k[16]) | mask(k[17]) << 8 | mask(k[18]) << 16 | mask(k[19]) << 24); + long j12 = mask & (mask(k[20]) | mask(k[21]) << 8 | mask(k[22]) << 16 | mask(k[23]) << 24); + long j13 = mask & (mask(k[24]) | mask(k[25]) << 8 | mask(k[26]) << 16 | mask(k[27]) << 24); + long j14 = mask & (mask(k[28]) | mask(k[29]) << 8 | mask(k[30]) << 16 | mask(k[31]) << 24); + long j15 = mask & (mask(c[12]) | mask(c[13]) << 8 | mask(c[14]) << 16 | mask(c[15]) << 24); + + long x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4; + long x5 = j5, x6 = j6, x7 = j7, x8 = j8; + long x9 = j9, x10 = j10, x11 = j11, x12 = j12; + long x13 = j13, x14 = j14, x15 = j15; + + for (int i = 0; i < rounds; i += 2) { + long u = mask & (x0 + x12); + x4 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x4 + x0); + x8 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x8 + x4); + x12 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x12 + x8); + x0 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x5 + x1); + x9 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x9 + x5); + x13 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x13 + x9); + x1 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x1 + x13); + x5 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x10 + x6); + x14 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x14 + x10); + x2 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x2 + x14); + x6 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x6 + x2); + x10 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x15 + x11); + x3 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x3 + x15); + x7 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x7 + x3); + x11 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x11 + x7); + x15 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x0 + x3); + x1 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x1 + x0); + x2 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x2 + x1); + x3 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x3 + x2); + x0 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x5 + x4); + x6 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x6 + x5); + x7 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x7 + x6); + x4 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x4 + x7); + x5 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x10 + x9); + x11 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x11 + x10); + x8 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x8 + x11); + x9 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x9 + x8); + x10 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x15 + x14); + x12 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x12 + x15); + x13 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x13 + x12); + x14 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x14 + x13); + x15 ^= mask & (u << 18 | u >>> (32 - 18)); + } + + x0 += j0; + x1 += j1; + x2 += j2; + x3 += j3; + x4 += j4; + x5 += j5; + x6 += j6; + x7 += j7; + x8 += j8; + x9 += j9; + x10 += j10; + x11 += j11; + x12 += j12; + x13 += j13; + x14 += j14; + x15 += j15; + + x0 &= mask; + x1 &= mask; + x2 &= mask; + x3 &= mask; + x4 &= mask; + x5 &= mask; + x6 &= mask; + x7 &= mask; + x8 &= mask; + x9 &= mask; + x10 &= mask; + x11 &= mask; + x12 &= mask; + x13 &= mask; + x14 &= mask; + x15 &= mask; + + out[0] = (byte) (x0); + out[1] = (byte) (x0 >> 8); + out[2] = (byte) (x0 >> 16); + out[3] = (byte) (x0 >> 24); + + out[4] = (byte) (x1); + out[5] = (byte) (x1 >> 8); + out[6] = (byte) (x1 >> 16); + out[7] = (byte) (x1 >> 24); + + out[8] = (byte) (x2); + out[9] = (byte) (x2 >> 8); + out[10] = (byte) (x2 >> 16); + out[11] = (byte) (x2 >> 24); + + out[12] = (byte) (x3); + out[13] = (byte) (x3 >> 8); + out[14] = (byte) (x3 >> 16); + out[15] = (byte) (x3 >> 24); + + out[16] = (byte) (x4); + out[17] = (byte) (x4 >> 8); + out[18] = (byte) (x4 >> 16); + out[19] = (byte) (x4 >> 24); + + out[20] = (byte) (x5); + out[21] = (byte) (x5 >> 8); + out[22] = (byte) (x5 >> 16); + out[23] = (byte) (x5 >> 24); + + out[24] = (byte) (x6); + out[25] = (byte) (x6 >> 8); + out[26] = (byte) (x6 >> 16); + out[27] = (byte) (x6 >> 24); + + out[28] = (byte) (x7); + out[29] = (byte) (x7 >> 8); + out[30] = (byte) (x7 >> 16); + out[31] = (byte) (x7 >> 24); + + out[32] = (byte) (x8); + out[33] = (byte) (x8 >> 8); + out[34] = (byte) (x8 >> 16); + out[35] = (byte) (x8 >> 24); + + out[36] = (byte) (x9); + out[37] = (byte) (x9 >> 8); + out[38] = (byte) (x9 >> 16); + out[39] = (byte) (x9 >> 24); + + out[40] = (byte) (x10); + out[41] = (byte) (x10 >> 8); + out[42] = (byte) (x10 >> 16); + out[43] = (byte) (x10 >> 24); + + out[44] = (byte) (x11); + out[45] = (byte) (x11 >> 8); + out[46] = (byte) (x11 >> 16); + out[47] = (byte) (x11 >> 24); + + out[48] = (byte) (x12); + out[49] = (byte) (x12 >> 8); + out[50] = (byte) (x12 >> 16); + out[51] = (byte) (x12 >> 24); + + out[52] = (byte) (x13); + out[53] = (byte) (x13 >> 8); + out[54] = (byte) (x13 >> 16); + out[55] = (byte) (x13 >> 24); + + out[56] = (byte) (x14); + out[57] = (byte) (x14 >> 8); + out[58] = (byte) (x14 >> 16); + out[59] = (byte) (x14 >> 24); + + out[60] = (byte) (x15); + out[61] = (byte) (x15 >> 8); + out[62] = (byte) (x15 >> 16); + out[63] = (byte) (x15 >> 24); + return out; + } + + // XORKeyStream crypts bytes from in to out using the given key and counters. + // In and out may be the same slice but otherwise should not overlap. Counter + // contains the raw salsa20 counter bytes (both nonce and block counter). + public static byte[] XORKeyStream(byte in[], byte counter[], byte key[]) { + byte out[] = in.clone(); + byte block[]; + byte counterCopy[] = counter.clone(); + + int count = 0; + while (in.length >= 64) { + block = core(counterCopy, key, SIGMA); + + for (int i = 0; i < block.length; i++) { + byte x = block[i]; + out[i + 64 * count] = (byte) (in[i] ^ x); + } + long u = 1; + for (int i = 8; i < 16; i++) { + u += 0xFF & counterCopy[i]; + counterCopy[i] = (byte) (u); + u >>= 8; + } + byte temp[] = in.clone(); + in = new byte[in.length - 64]; + for (int i = 0; i < in.length; i++) { + in[i] = temp[i + 64]; + } + + count++; + } + + if (in.length > 0) { + block = core(counterCopy, key, SIGMA); + + for (int i = 0; i < in.length; i++) { + out[i + count * 64] = (byte) (in[i] ^ block[i]); + } + } + + return out; + } + + // HSalsa20 applies the HSalsa20 core function to a 16-byte input in, 32-byte + // key k, and 16-byte constant c, and returns the result as the 32-byte array + // out. + public static byte[] HSalsa20(byte[] in, byte[] k, byte[] c) { + long x0 = mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24; + long x1 = mask(k[0]) | mask(k[1]) << 8 | mask(k[2]) << 16 | mask(k[3]) << 24; + long x2 = mask(k[4]) | mask(k[5]) << 8 | mask(k[6]) << 16 | mask(k[7]) << 24; + long x3 = mask(k[8]) | mask(k[9]) << 8 | mask(k[10]) << 16 | mask(k[11]) << 24; + long x4 = mask(k[12]) | mask(k[13]) << 8 | mask(k[14]) << 16 | mask(k[15]) << 24; + long x5 = mask(c[4]) | mask(c[5]) << 8 | mask(c[6]) << 16 | mask(c[7]) << 24; + long x6 = mask(in[0]) | mask(in[1]) << 8 | mask(in[2]) << 16 | mask(in[3]) << 24; + long x7 = mask(in[4]) | mask(in[5]) << 8 | mask(in[6]) << 16 | mask(in[7]) << 24; + long x8 = mask(in[8]) | mask(in[9]) << 8 | mask(in[10]) << 16 | mask(in[11]) << 24; + long x9 = mask(in[12]) | mask(in[13]) << 8 | mask(in[14]) << 16 | mask(in[15]) << 24; + long x10 = mask(c[8]) | mask(c[9]) << 8 | mask(c[10]) << 16 | mask(c[11]) << 24; + long x11 = mask(k[16]) | mask(k[17]) << 8 | mask(k[18]) << 16 | mask(k[19]) << 24; + long x12 = mask(k[20]) | mask(k[21]) << 8 | mask(k[22]) << 16 | mask(k[23]) << 24; + long x13 = mask(k[24]) | mask(k[25]) << 8 | mask(k[26]) << 16 | mask(k[27]) << 24; + long x14 = mask(k[28]) | mask(k[29]) << 8 | mask(k[30]) << 16 | mask(k[31]) << 24; + long x15 = mask(c[12]) | mask(c[13]) << 8 | mask(c[14]) << 16 | mask(c[15]) << 24; + + long mask = 0xFFFFFFFFl; + for (int i = 0; i < 20; i += 2) { + long u = mask & (x0 + x12); + x4 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x4 + x0); + x8 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x8 + x4); + x12 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x12 + x8); + x0 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x5 + x1); + x9 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x9 + x5); + x13 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x13 + x9); + x1 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x1 + x13); + x5 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x10 + x6); + x14 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x14 + x10); + x2 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x2 + x14); + x6 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x6 + x2); + x10 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x15 + x11); + x3 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x3 + x15); + x7 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x7 + x3); + x11 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x11 + x7); + x15 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x0 + x3); + x1 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x1 + x0); + x2 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x2 + x1); + x3 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x3 + x2); + x0 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x5 + x4); + x6 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x6 + x5); + x7 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x7 + x6); + x4 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x4 + x7); + x5 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x10 + x9); + x11 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x11 + x10); + x8 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x8 + x11); + x9 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x9 + x8); + x10 ^= mask & (u << 18 | u >>> (32 - 18)); + + u = mask & (x15 + x14); + x12 ^= mask & (u << 7 | u >>> (32 - 7)); + u = mask & (x12 + x15); + x13 ^= mask & (u << 9 | u >>> (32 - 9)); + u = mask & (x13 + x12); + x14 ^= mask & (u << 13 | u >>> (32 - 13)); + u = mask & (x14 + x13); + x15 ^= mask & (u << 18 | u >>> (32 - 18)); + } + + byte out[] = new byte[32]; + out[0] = (byte) x0; + out[1] = (byte) (x0 >> 8); + out[2] = (byte) (x0 >> 16); + out[3] = (byte) (x0 >> 24); + + out[4] = (byte) (x5); + out[5] = (byte) (x5 >> 8); + out[6] = (byte) (x5 >> 16); + out[7] = (byte) (x5 >> 24); + + out[8] = (byte) (x10); + out[9] = (byte) (x10 >> 8); + out[10] = (byte) (x10 >> 16); + out[11] = (byte) (x10 >> 24); + + out[12] = (byte) (x15); + out[13] = (byte) (x15 >> 8); + out[14] = (byte) (x15 >> 16); + out[15] = (byte) (x15 >> 24); + + out[16] = (byte) (x6); + out[17] = (byte) (x6 >> 8); + out[18] = (byte) (x6 >> 16); + out[19] = (byte) (x6 >> 24); + + out[20] = (byte) (x7); + out[21] = (byte) (x7 >> 8); + out[22] = (byte) (x7 >> 16); + out[23] = (byte) (x7 >> 24); + + out[24] = (byte) (x8); + out[25] = (byte) (x8 >> 8); + out[26] = (byte) (x8 >> 16); + out[27] = (byte) (x8 >> 24); + + out[28] = (byte) (x9); + out[29] = (byte) (x9 >> 8); + out[30] = (byte) (x9 >> 16); + out[31] = (byte) (x9 >> 24); + return out; + } +} diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java new file mode 100644 index 00000000..09820eb4 --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -0,0 +1,137 @@ +/* +Copyright 2015 Eve Freeman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +package com.pusher.client.crypto.nacl; + +import java.util.Arrays; + +public class SecretBoxOpener { + + private static final int OVERHEAD = Poly1305.TAG_SIZE; + + private byte[] key; + + public SecretBoxOpener(byte[] key) { + // TODO: add a a little preconditions lib/class for that + if (key == null) throw new IllegalArgumentException("null key passed"); + if (key.length != 32) { + throw new IllegalArgumentException("key should be 32B, is: " + key.length + "B"); + } + + this.key = key; + } + + public byte[] open(byte box[], byte nonce[]) throws AuthenticityException { + if (key == null) { + throw new IllegalStateException("key has been cleared, create new instance"); + } + + byte subKey[] = new byte[32]; + byte counter[] = new byte[16]; + setup(subKey, counter, nonce, key); + + // The Poly1305 key is generated by encrypting 32 bytes of zeros. Since + // Salsa20 works with 64-byte blocks, we also generate 32 bytes of + // keystream as a side effect. + byte firstBlock[] = new byte[64]; + for (int i = 0; i < firstBlock.length; i++) { + firstBlock[i] = 0; + } + firstBlock = Salsa.XORKeyStream(firstBlock, counter, subKey); + + byte poly1305Key[] = new byte[32]; + for (int i = 0; i < poly1305Key.length; i++) { + poly1305Key[i] = firstBlock[i]; + } + byte tag[] = new byte[Poly1305.TAG_SIZE]; + for (int i = 0; i < tag.length; i++) { + tag[i] = box[i]; + } + + byte cipher[] = new byte[box.length - Poly1305.TAG_SIZE]; + for (int i = 0; i < cipher.length; i++) { + cipher[i] = box[i + Poly1305.TAG_SIZE]; + } + if (!Poly1305.verify(tag, cipher, poly1305Key)) { + throw new AuthenticityException(); + } + + byte ret[] = new byte[box.length - OVERHEAD]; + for (int i = 0; i < ret.length; i++) { + ret[i] = box[i + OVERHEAD]; + } + // We XOR up to 32 bytes of box with the keystream generated from + // the first block. + byte firstMessageBlock[] = new byte[ret.length]; + if (ret.length > 32) { + firstMessageBlock = new byte[32]; + } + for (int i = 0; i < firstMessageBlock.length; i++) { + firstMessageBlock[i] = ret[i]; + } + for (int i = 0; i < firstMessageBlock.length; i++) { + ret[i] = (byte) (firstBlock[32 + i] ^ firstMessageBlock[i]); + } + + counter[8] = 1; + byte newbox[] = new byte[box.length - (firstMessageBlock.length + OVERHEAD)]; + for (int i = 0; i < newbox.length; i++) { + newbox[i] = box[i + firstMessageBlock.length + OVERHEAD]; + } + byte rest[] = Salsa.XORKeyStream(newbox, counter, subKey); + // Now decrypt the rest. + + for (int i = firstMessageBlock.length; i < ret.length; i++) { + ret[i] = rest[i - firstMessageBlock.length]; + } + return ret; + } + + public void clearKey() { + Arrays.fill(key, (byte) 0); + if (key[0] != 0) { + throw new SecurityException("key not cleared correctly"); + } + key = null; + // TODO: ensure implemented securely (so that the clearing code + // is not removed by compiler's optimisations) + } + + // subKey = byte[32], counter = byte[16], nonce = byte[24], key = byte[32] + private void setup(byte subKey[], byte counter[], byte nonce[], byte key[]) { + // We use XSalsa20 for encryption so first we need to generate a + // key and nonce with HSalsa20. + byte hNonce[] = new byte[16]; + for (int i = 0; i < hNonce.length; i++) { + hNonce[i] = nonce[i]; + } + byte newSubKey[] = Salsa.HSalsa20(hNonce, key, Salsa.SIGMA); + for (int i = 0; i < subKey.length; i++) { + subKey[i] = newSubKey[i]; + } + + for (int i = 0; i < nonce.length - 16; i++) { + counter[i] = nonce[i + 16]; + } + } +} diff --git a/src/main/java/com/pusher/client/crypto/nacl/Subtle.java b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java new file mode 100644 index 00000000..ccfa3950 --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java @@ -0,0 +1,45 @@ +/* +Copyright 2015 Eve Freeman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +*/ + +package com.pusher.client.crypto.nacl; + +public class Subtle { + public static boolean constantTimeCompare(byte x[], byte y[]) { + if (x.length != y.length) { + return false; + } + byte v = 0; + for (int i = 0; i < x.length; i++) { + v |= x[i] ^ y[i]; + } + return constantTimeByteEq(v, (byte) 0); + } + + public static boolean constantTimeByteEq(byte x, byte y) { + byte z = (byte) ~(x ^ y); + z &= (byte) (z >> 4); + z &= (byte) (z >> 2); + z &= (byte) (z >> 1); + return z == -1; + } +} From 451a3f232fbcb1aa6359fa2b69368c1ae149171f Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:32:45 +0000 Subject: [PATCH 02/48] Add copyright for Pusher Following on Danielle's feedback: https://github.com/pusher/pusher-websocket-java/pull/235/files#r397189111 --- src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index 09820eb4..fb2ecb04 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -1,4 +1,5 @@ /* +Copyright 2020 Pusher Ltd Copyright 2015 Eve Freeman Permission is hereby granted, free of charge, to any person obtaining From a336ab7bcf7f504ff69b0ed4a6fbaa89b93ce99a Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 24 Mar 2020 15:54:16 +0000 Subject: [PATCH 03/48] Exclude crypt/nacl package from Javadoc --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8031c50d..5f7dc3ce 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ javadoc { exclude "**/com/pusher/client/channel/impl/*" exclude "**/com/pusher/client/connection/impl/*" exclude "**/com/pusher/client/connection/websocket/*" + exclude "**/com/pusher/client/crypto/nacl/*" exclude "**/org/java_websocket/*" exclude "**/com/pusher/client/example/*" options.linkSource = true From d851213c8d2a62934de3d18bc777dd6586151abf Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 25 Mar 2020 15:45:42 +0000 Subject: [PATCH 04/48] Add Base64 decoding and test for decryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Truth assertion lib — approved by Mike. --- build.gradle | 3 ++ .../java/com/pusher/client/util/Base64.java | 39 +++++++++++++++++++ .../crypto/nacl/SecretBoxOpenerTest.java | 28 +++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/main/java/com/pusher/client/util/Base64.java create mode 100644 src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java diff --git a/build.gradle b/build.gradle index 5f7dc3ce..bc53e50e 100644 --- a/build.gradle +++ b/build.gradle @@ -44,9 +44,12 @@ repositories { dependencies { compile "com.google.code.gson:gson:2.2.2" compile "org.java-websocket:Java-WebSocket:1.4.0" + testCompile "org.mockito:mockito-all:1.8.5" testCompile "org.powermock:powermock-module-junit4:1.4.11" testCompile "org.powermock:powermock-api-mockito:1.4.11" + + testImplementation "com.google.truth:truth:1.0.1" } diff --git a/src/main/java/com/pusher/client/util/Base64.java b/src/main/java/com/pusher/client/util/Base64.java new file mode 100644 index 00000000..0da3d12c --- /dev/null +++ b/src/main/java/com/pusher/client/util/Base64.java @@ -0,0 +1,39 @@ +package com.pusher.client.util; + +// copied from: https://stackoverflow.com/a/4265472/501940 +public class Base64 { + + private final static char[] ALPHABET = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + private static int[] toInt = new int[128]; + + static { + for (int i = 0; i < ALPHABET.length; i++) { + toInt[ALPHABET[i]] = i; + } + } + + public static byte[] decode(String base64String) { + int delta = base64String.endsWith("==") ? 2 : base64String.endsWith("=") ? 1 : 0; + byte[] buffer = new byte[base64String.length() * 3 / 4 - delta]; + int mask = 0xFF; + int index = 0; + for (int i = 0; i < base64String.length(); i += 4) { + int c0 = toInt[base64String.charAt(i)]; + int c1 = toInt[base64String.charAt(i + 1)]; + buffer[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask); + if (index >= buffer.length) { + return buffer; + } + int c2 = toInt[base64String.charAt(i + 2)]; + buffer[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask); + if (index >= buffer.length) { + return buffer; + } + int c3 = toInt[base64String.charAt(i + 3)]; + buffer[index++] = (byte) (((c2 << 6) | c3) & mask); + } + return buffer; + } +} diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java new file mode 100644 index 00000000..034dfd83 --- /dev/null +++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java @@ -0,0 +1,28 @@ +package com.pusher.client.crypto.nacl; + +import static com.google.common.truth.Truth.assertThat; + +import com.pusher.client.util.Base64; +import org.junit.Before; +import org.junit.Test; + +public class SecretBoxOpenerTest { + + byte[] key = Base64.decode("6071zp2l/GPnDPDXNWTJDHyIZ8pZMvQrYsa4xuTKK2c="); + SecretBoxOpener subject; + + @Before + public void setUp() { + subject = new SecretBoxOpener(key); + } + + @Test + public void open() { + byte[] cipher = Base64.decode("tvttPE2PRQp0bWDmaPyiEU8YJGztmTvTN77OoPwftTNTdDgJXwxHQPE="); + byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p"); + + byte[] clearText = subject.open(cipher, nonce); + + assertThat(new String(clearText)).isEqualTo("{\"message\":\"hello world\"}"); + } +} \ No newline at end of file From ce14b65b4b029b746790e53f43fc0733efcebe9a Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 26 Mar 2020 15:32:37 +0000 Subject: [PATCH 05/48] Add Base64 char validation Improve naming Follow up Mike's feedback: https://github.com/pusher/pusher-websocket-java/pull/236#discussion_r397986501 --- .../java/com/pusher/client/util/Base64.java | 31 ++++++++++++------- .../com/pusher/client/util/Base64Test.java | 27 ++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/pusher/client/util/Base64Test.java diff --git a/src/main/java/com/pusher/client/util/Base64.java b/src/main/java/com/pusher/client/util/Base64.java index 0da3d12c..67350935 100644 --- a/src/main/java/com/pusher/client/util/Base64.java +++ b/src/main/java/com/pusher/client/util/Base64.java @@ -1,37 +1,46 @@ package com.pusher.client.util; -// copied from: https://stackoverflow.com/a/4265472/501940 +import static java.util.Arrays.fill; + +// copied from: https://stackoverflow.com/a/4265472/501940 and improved (naming, char validation) public class Base64 { - private final static char[] ALPHABET = + private final static char[] CHAR_INDEX_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); - private static int[] toInt = new int[128]; + private static int[] charToIndexSparseMappingArray = new int[128]; static { - for (int i = 0; i < ALPHABET.length; i++) { - toInt[ALPHABET[i]] = i; + fill(charToIndexSparseMappingArray, -1); + for (int i = 0; i < CHAR_INDEX_TABLE.length; i++) { + charToIndexSparseMappingArray[CHAR_INDEX_TABLE[i]] = i; } } + private static int toInt(char character) { + int retVal = charToIndexSparseMappingArray[character]; + if (retVal == -1) throw new IllegalArgumentException("invalid char: " + character); + return retVal; + } + public static byte[] decode(String base64String) { - int delta = base64String.endsWith("==") ? 2 : base64String.endsWith("=") ? 1 : 0; - byte[] buffer = new byte[base64String.length() * 3 / 4 - delta]; + int paddingSize = base64String.endsWith("==") ? 2 : base64String.endsWith("=") ? 1 : 0; + byte[] buffer = new byte[base64String.length() * 3 / 4 - paddingSize]; int mask = 0xFF; int index = 0; for (int i = 0; i < base64String.length(); i += 4) { - int c0 = toInt[base64String.charAt(i)]; - int c1 = toInt[base64String.charAt(i + 1)]; + int c0 = toInt(base64String.charAt(i)); + int c1 = toInt(base64String.charAt(i + 1)); buffer[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask); if (index >= buffer.length) { return buffer; } - int c2 = toInt[base64String.charAt(i + 2)]; + int c2 = toInt(base64String.charAt(i + 2)); buffer[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask); if (index >= buffer.length) { return buffer; } - int c3 = toInt[base64String.charAt(i + 3)]; + int c3 = toInt(base64String.charAt(i + 3)); buffer[index++] = (byte) (((c2 << 6) | c3) & mask); } return buffer; diff --git a/src/test/java/com/pusher/client/util/Base64Test.java b/src/test/java/com/pusher/client/util/Base64Test.java new file mode 100644 index 00000000..28439fc7 --- /dev/null +++ b/src/test/java/com/pusher/client/util/Base64Test.java @@ -0,0 +1,27 @@ +package com.pusher.client.util; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class Base64Test { + + @Test + public void decodeValidChars() { + String validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + assertThat(Base64.decode(validChars)).isNotEmpty(); + } + + // https://en.wikipedia.org/wiki/Base64#URL_applications + @Test(expected = IllegalArgumentException.class) + public void failDecodingMinusChar() { + Base64.decode("-"); + } + + // https://en.wikipedia.org/wiki/Base64#URL_applications + @Test(expected = IllegalArgumentException.class) + public void failDecodingUnderscoreChar() { + Base64.decode("_"); + } + +} From 1d43fdba6750a6af6907b2c2c20aa94717132e19 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 26 Mar 2020 16:03:37 +0000 Subject: [PATCH 06/48] Use a better term As it's not a temporary store for I/O. --- .../java/com/pusher/client/util/Base64.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/pusher/client/util/Base64.java b/src/main/java/com/pusher/client/util/Base64.java index 67350935..44d5431a 100644 --- a/src/main/java/com/pusher/client/util/Base64.java +++ b/src/main/java/com/pusher/client/util/Base64.java @@ -25,24 +25,24 @@ private static int toInt(char character) { public static byte[] decode(String base64String) { int paddingSize = base64String.endsWith("==") ? 2 : base64String.endsWith("=") ? 1 : 0; - byte[] buffer = new byte[base64String.length() * 3 / 4 - paddingSize]; + byte[] retVal = new byte[base64String.length() * 3 / 4 - paddingSize]; int mask = 0xFF; int index = 0; for (int i = 0; i < base64String.length(); i += 4) { int c0 = toInt(base64String.charAt(i)); int c1 = toInt(base64String.charAt(i + 1)); - buffer[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask); - if (index >= buffer.length) { - return buffer; + retVal[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask); + if (index >= retVal.length) { + return retVal; } int c2 = toInt(base64String.charAt(i + 2)); - buffer[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask); - if (index >= buffer.length) { - return buffer; + retVal[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask); + if (index >= retVal.length) { + return retVal; } int c3 = toInt(base64String.charAt(i + 3)); - buffer[index++] = (byte) (((c2 << 6) | c3) & mask); + retVal[index++] = (byte) (((c2 << 6) | c3) & mask); } - return buffer; + return retVal; } } From d035064773d069b3b48ff82e8b5ffa80fa2220ff Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 26 Mar 2020 21:21:57 +0000 Subject: [PATCH 07/48] Exclude Base64 class from javadoc Simplify exclude rules making it more clear they refer to package names not file paths relative to project's root. --- build.gradle | 13 +++++++------ .../pusher/client/util/{ => internal}/Base64.java | 2 +- .../client/crypto/nacl/SecretBoxOpenerTest.java | 2 +- .../java/com/pusher/client/util/Base64Test.java | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) rename src/main/java/com/pusher/client/util/{ => internal}/Base64.java (97%) diff --git a/build.gradle b/build.gradle index bc53e50e..1cb0eb47 100644 --- a/build.gradle +++ b/build.gradle @@ -66,12 +66,13 @@ javadoc { options.overview = file("src/main/javadoc/overview.html") // uncomment this to use the custom javadoc styles //options.stylesheetFile = file("src/main/javadoc/css/styles.css") - exclude "**/com/pusher/client/channel/impl/*" - exclude "**/com/pusher/client/connection/impl/*" - exclude "**/com/pusher/client/connection/websocket/*" - exclude "**/com/pusher/client/crypto/nacl/*" - exclude "**/org/java_websocket/*" - exclude "**/com/pusher/client/example/*" + exclude "com/pusher/client/channel/impl/*" + exclude "com/pusher/client/connection/impl/*" + exclude "com/pusher/client/connection/websocket/*" + exclude "com/pusher/client/crypto/nacl/*" + exclude "com/pusher/client/util/internal/*" + exclude "org/java_websocket/*" + exclude "com/pusher/client/example/*" options.linkSource = true } diff --git a/src/main/java/com/pusher/client/util/Base64.java b/src/main/java/com/pusher/client/util/internal/Base64.java similarity index 97% rename from src/main/java/com/pusher/client/util/Base64.java rename to src/main/java/com/pusher/client/util/internal/Base64.java index 44d5431a..24b506aa 100644 --- a/src/main/java/com/pusher/client/util/Base64.java +++ b/src/main/java/com/pusher/client/util/internal/Base64.java @@ -1,4 +1,4 @@ -package com.pusher.client.util; +package com.pusher.client.util.internal; import static java.util.Arrays.fill; diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java index 034dfd83..e234ed6a 100644 --- a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java +++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java @@ -2,7 +2,7 @@ import static com.google.common.truth.Truth.assertThat; -import com.pusher.client.util.Base64; +import com.pusher.client.util.internal.Base64; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/com/pusher/client/util/Base64Test.java b/src/test/java/com/pusher/client/util/Base64Test.java index 28439fc7..0f027fa8 100644 --- a/src/test/java/com/pusher/client/util/Base64Test.java +++ b/src/test/java/com/pusher/client/util/Base64Test.java @@ -2,6 +2,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.pusher.client.util.internal.Base64; import org.junit.Test; public class Base64Test { From 5438e6c708143fb8055862aa1f07a5e9c29aa984 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Mon, 30 Mar 2020 16:21:22 +0100 Subject: [PATCH 08/48] Add a test for crypto authenticity failure --- .../client/crypto/nacl/SecretBoxOpenerTest.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java index e234ed6a..5b02c3b7 100644 --- a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java +++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java @@ -1,6 +1,7 @@ package com.pusher.client.crypto.nacl; import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.copyOf; import com.pusher.client.util.internal.Base64; import org.junit.Before; @@ -9,6 +10,10 @@ public class SecretBoxOpenerTest { byte[] key = Base64.decode("6071zp2l/GPnDPDXNWTJDHyIZ8pZMvQrYsa4xuTKK2c="); + + byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p"); + byte[] cipher = Base64.decode("tvttPE2PRQp0bWDmaPyiEU8YJGztmTvTN77OoPwftTNTdDgJXwxHQPE="); + SecretBoxOpener subject; @Before @@ -18,11 +23,16 @@ public void setUp() { @Test public void open() { - byte[] cipher = Base64.decode("tvttPE2PRQp0bWDmaPyiEU8YJGztmTvTN77OoPwftTNTdDgJXwxHQPE="); - byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p"); - byte[] clearText = subject.open(cipher, nonce); assertThat(new String(clearText)).isEqualTo("{\"message\":\"hello world\"}"); } + + @Test(expected = AuthenticityException.class) + public void openFailsForTamperedCipher() { + byte[] tamperedCipher = copyOf(cipher, cipher.length); + tamperedCipher[0] ^= tamperedCipher[0]; + + subject.open(tamperedCipher, nonce); + } } \ No newline at end of file From b539a3e67f53166e4323dae9293b3c75a35c00eb Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Mon, 30 Mar 2020 19:03:21 +0100 Subject: [PATCH 09/48] Add precondition check util --- .../client/crypto/nacl/SecretBoxOpener.java | 14 +++++------ .../client/util/internal/Preconditions.java | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/pusher/client/util/internal/Preconditions.java diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index fb2ecb04..5bf212bc 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -24,6 +24,9 @@ a copy of this software and associated documentation files (the "Software"), package com.pusher.client.crypto.nacl; +import static com.pusher.client.util.internal.Preconditions.checkArgument; +import static com.pusher.client.util.internal.Preconditions.checkNotNull; + import java.util.Arrays; public class SecretBoxOpener { @@ -33,19 +36,14 @@ public class SecretBoxOpener { private byte[] key; public SecretBoxOpener(byte[] key) { - // TODO: add a a little preconditions lib/class for that - if (key == null) throw new IllegalArgumentException("null key passed"); - if (key.length != 32) { - throw new IllegalArgumentException("key should be 32B, is: " + key.length + "B"); - } + checkNotNull(key, "null key passed"); + checkArgument(key.length == 32, "key should be 32B, is: " + key.length + "B"); this.key = key; } public byte[] open(byte box[], byte nonce[]) throws AuthenticityException { - if (key == null) { - throw new IllegalStateException("key has been cleared, create new instance"); - } + checkNotNull(key, "key has been cleared, create new instance"); byte subKey[] = new byte[32]; byte counter[] = new byte[16]; diff --git a/src/main/java/com/pusher/client/util/internal/Preconditions.java b/src/main/java/com/pusher/client/util/internal/Preconditions.java new file mode 100644 index 00000000..9841efe4 --- /dev/null +++ b/src/main/java/com/pusher/client/util/internal/Preconditions.java @@ -0,0 +1,23 @@ +package com.pusher.client.util.internal; + +public class Preconditions { + + public static void checkArgument(boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + } + + public static void checkState(boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } + + public static T checkNotNull(T reference, Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; + } +} From dbdea4e53d468051462e3e7480e7a0a5368d174d Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 31 Mar 2020 11:41:38 +0100 Subject: [PATCH 10/48] Alter precondition failure massage Per feedback: https://github.com/pusher/pusher-websocket-java/pull/240/files#r400806116 --- .../java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index 5bf212bc..32048f73 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -37,7 +37,8 @@ public class SecretBoxOpener { public SecretBoxOpener(byte[] key) { checkNotNull(key, "null key passed"); - checkArgument(key.length == 32, "key should be 32B, is: " + key.length + "B"); + checkArgument(key.length == 32, "key length should be 32 bytes, but is " + + key.length + " bytes"); this.key = key; } From bb925cd1710e95e7e2e89f69544ce3eb738455d4 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 31 Mar 2020 12:03:10 +0100 Subject: [PATCH 11/48] Use "must" instead of "should" for precondition failure Per feedback: https://github.com/pusher/pusher-websocket-java/pull/240#discussion_r400816216 --- .../java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index 32048f73..dd5f8a8b 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -37,7 +37,7 @@ public class SecretBoxOpener { public SecretBoxOpener(byte[] key) { checkNotNull(key, "null key passed"); - checkArgument(key.length == 32, "key length should be 32 bytes, but is " + + checkArgument(key.length == 32, "key length must be 32 bytes, but is " + key.length + " bytes"); this.key = key; From 428bca0d65e5735f3f5d2ea995404d5c3b00cd69 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:38:00 +0100 Subject: [PATCH 12/48] Fix warnings about C-style array declarations in NaCl --- .../pusher/client/crypto/nacl/Poly1305.java | 12 ++++---- .../com/pusher/client/crypto/nacl/Salsa.java | 16 +++++------ .../client/crypto/nacl/SecretBoxOpener.java | 28 +++++++++---------- .../com/pusher/client/crypto/nacl/Subtle.java | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index 307ebcf7..365c8f13 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -57,8 +57,8 @@ private static double longBitsToDouble(long bits) { return (double) s * (double) m * Math.pow(2, e - 1075); } - public static boolean verify(byte mac[], byte m[], byte key[]) { - byte tmp[] = sum(m, key); + public static boolean verify(byte[] mac, byte[] m, byte[] key) { + byte[] tmp = sum(m, key); //Util.printHex("tmp", tmp); //Util.printHex("mac", mac); return Subtle.constantTimeCompare(tmp, mac); @@ -67,9 +67,9 @@ public static boolean verify(byte mac[], byte m[], byte key[]) { // Sum generates an authenticator for m using a one-time key and puts the // 16-byte result into out. Authenticating two different messages with the same // key allows an attacker to forge messages at will. - public static byte[] sum(byte m[], byte key[]) { - byte r[] = key.clone(); - byte s[] = new byte[16]; + public static byte[] sum(byte[] m, byte[] key) { + byte[] r = key.clone(); + byte[] s = new byte[16]; for (int i = 0; i < s.length; i++) { s[i] = key[i + 16]; } @@ -1516,7 +1516,7 @@ public static byte[] sum(byte m[], byte key[]) { f2 += (s23); f3 += s33; - byte out[] = new byte[16]; + byte[] out = new byte[16]; out[0] = (byte) (f0); f0 >>= 8; out[1] = (byte) (f0); diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java index a95e214c..40f64f02 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java @@ -34,8 +34,8 @@ private static long mask(byte x) { // core applies the Salsa20 core function to 16-byte input in, 32-byte key k, // and 16-byte constant c, and puts the result into 64-byte array out. - public static byte[] core(byte in[], byte k[], byte c[]) { - byte out[] = new byte[64]; + public static byte[] core(byte[] in, byte[] k, byte[] c) { + byte[] out = new byte[64]; long mask = 0xFFFFFFFFl; long j0 = mask & (mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24); @@ -253,10 +253,10 @@ public static byte[] core(byte in[], byte k[], byte c[]) { // XORKeyStream crypts bytes from in to out using the given key and counters. // In and out may be the same slice but otherwise should not overlap. Counter // contains the raw salsa20 counter bytes (both nonce and block counter). - public static byte[] XORKeyStream(byte in[], byte counter[], byte key[]) { - byte out[] = in.clone(); - byte block[]; - byte counterCopy[] = counter.clone(); + public static byte[] XORKeyStream(byte[] in, byte[] counter, byte[] key) { + byte[] out = in.clone(); + byte[] block; + byte[] counterCopy = counter.clone(); int count = 0; while (in.length >= 64) { @@ -272,7 +272,7 @@ public static byte[] XORKeyStream(byte in[], byte counter[], byte key[]) { counterCopy[i] = (byte) (u); u >>= 8; } - byte temp[] = in.clone(); + byte[] temp = in.clone(); in = new byte[in.length - 64]; for (int i = 0; i < in.length; i++) { in[i] = temp[i + 64]; @@ -388,7 +388,7 @@ public static byte[] HSalsa20(byte[] in, byte[] k, byte[] c) { x15 ^= mask & (u << 18 | u >>> (32 - 18)); } - byte out[] = new byte[32]; + byte[] out = new byte[32]; out[0] = (byte) x0; out[1] = (byte) (x0 >> 8); out[2] = (byte) (x0 >> 16); diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index dd5f8a8b..0a5048d1 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -43,32 +43,32 @@ public SecretBoxOpener(byte[] key) { this.key = key; } - public byte[] open(byte box[], byte nonce[]) throws AuthenticityException { + public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { checkNotNull(key, "key has been cleared, create new instance"); - byte subKey[] = new byte[32]; - byte counter[] = new byte[16]; + byte[] subKey = new byte[32]; + byte[] counter = new byte[16]; setup(subKey, counter, nonce, key); // The Poly1305 key is generated by encrypting 32 bytes of zeros. Since // Salsa20 works with 64-byte blocks, we also generate 32 bytes of // keystream as a side effect. - byte firstBlock[] = new byte[64]; + byte[] firstBlock = new byte[64]; for (int i = 0; i < firstBlock.length; i++) { firstBlock[i] = 0; } firstBlock = Salsa.XORKeyStream(firstBlock, counter, subKey); - byte poly1305Key[] = new byte[32]; + byte[] poly1305Key = new byte[32]; for (int i = 0; i < poly1305Key.length; i++) { poly1305Key[i] = firstBlock[i]; } - byte tag[] = new byte[Poly1305.TAG_SIZE]; + byte[] tag = new byte[Poly1305.TAG_SIZE]; for (int i = 0; i < tag.length; i++) { tag[i] = box[i]; } - byte cipher[] = new byte[box.length - Poly1305.TAG_SIZE]; + byte[] cipher = new byte[box.length - Poly1305.TAG_SIZE]; for (int i = 0; i < cipher.length; i++) { cipher[i] = box[i + Poly1305.TAG_SIZE]; } @@ -76,13 +76,13 @@ public byte[] open(byte box[], byte nonce[]) throws AuthenticityException { throw new AuthenticityException(); } - byte ret[] = new byte[box.length - OVERHEAD]; + byte[] ret = new byte[box.length - OVERHEAD]; for (int i = 0; i < ret.length; i++) { ret[i] = box[i + OVERHEAD]; } // We XOR up to 32 bytes of box with the keystream generated from // the first block. - byte firstMessageBlock[] = new byte[ret.length]; + byte[] firstMessageBlock = new byte[ret.length]; if (ret.length > 32) { firstMessageBlock = new byte[32]; } @@ -94,11 +94,11 @@ public byte[] open(byte box[], byte nonce[]) throws AuthenticityException { } counter[8] = 1; - byte newbox[] = new byte[box.length - (firstMessageBlock.length + OVERHEAD)]; + byte[] newbox = new byte[box.length - (firstMessageBlock.length + OVERHEAD)]; for (int i = 0; i < newbox.length; i++) { newbox[i] = box[i + firstMessageBlock.length + OVERHEAD]; } - byte rest[] = Salsa.XORKeyStream(newbox, counter, subKey); + byte[] rest = Salsa.XORKeyStream(newbox, counter, subKey); // Now decrypt the rest. for (int i = firstMessageBlock.length; i < ret.length; i++) { @@ -118,14 +118,14 @@ public void clearKey() { } // subKey = byte[32], counter = byte[16], nonce = byte[24], key = byte[32] - private void setup(byte subKey[], byte counter[], byte nonce[], byte key[]) { + private void setup(byte[] subKey, byte[] counter, byte[] nonce, byte[] key) { // We use XSalsa20 for encryption so first we need to generate a // key and nonce with HSalsa20. - byte hNonce[] = new byte[16]; + byte[] hNonce = new byte[16]; for (int i = 0; i < hNonce.length; i++) { hNonce[i] = nonce[i]; } - byte newSubKey[] = Salsa.HSalsa20(hNonce, key, Salsa.SIGMA); + byte[] newSubKey = Salsa.HSalsa20(hNonce, key, Salsa.SIGMA); for (int i = 0; i < subKey.length; i++) { subKey[i] = newSubKey[i]; } diff --git a/src/main/java/com/pusher/client/crypto/nacl/Subtle.java b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java index ccfa3950..fbe2bf6f 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Subtle.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java @@ -24,7 +24,7 @@ a copy of this software and associated documentation files (the "Software"), package com.pusher.client.crypto.nacl; public class Subtle { - public static boolean constantTimeCompare(byte x[], byte y[]) { + public static boolean constantTimeCompare(byte[] x, byte[] y) { if (x.length != y.length) { return false; } From a97bcbd33940f4523dfbc1698a87b3b451052c79 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:58:03 +0100 Subject: [PATCH 13/48] Fix warnings about manual array copying Those are automatic IntelliJ's proposed fixes. --- .../pusher/client/crypto/nacl/Poly1305.java | 4 +- .../com/pusher/client/crypto/nacl/Salsa.java | 4 +- .../client/crypto/nacl/SecretBoxOpener.java | 38 ++++++------------- 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index 365c8f13..3ac071c1 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -70,9 +70,7 @@ public static boolean verify(byte[] mac, byte[] m, byte[] key) { public static byte[] sum(byte[] m, byte[] key) { byte[] r = key.clone(); byte[] s = new byte[16]; - for (int i = 0; i < s.length; i++) { - s[i] = key[i + 16]; - } + System.arraycopy(key, 16, s, 0, s.length); double y7; double y6; diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java index 40f64f02..1b013409 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java @@ -274,9 +274,7 @@ public static byte[] XORKeyStream(byte[] in, byte[] counter, byte[] key) { } byte[] temp = in.clone(); in = new byte[in.length - 64]; - for (int i = 0; i < in.length; i++) { - in[i] = temp[i + 64]; - } + System.arraycopy(temp, 64, in, 0, in.length); count++; } diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index 0a5048d1..686f3959 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -60,35 +60,25 @@ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { firstBlock = Salsa.XORKeyStream(firstBlock, counter, subKey); byte[] poly1305Key = new byte[32]; - for (int i = 0; i < poly1305Key.length; i++) { - poly1305Key[i] = firstBlock[i]; - } + System.arraycopy(firstBlock, 0, poly1305Key, 0, poly1305Key.length); byte[] tag = new byte[Poly1305.TAG_SIZE]; - for (int i = 0; i < tag.length; i++) { - tag[i] = box[i]; - } + System.arraycopy(box, 0, tag, 0, tag.length); byte[] cipher = new byte[box.length - Poly1305.TAG_SIZE]; - for (int i = 0; i < cipher.length; i++) { - cipher[i] = box[i + Poly1305.TAG_SIZE]; - } + System.arraycopy(box, 0 + Poly1305.TAG_SIZE, cipher, 0, cipher.length); if (!Poly1305.verify(tag, cipher, poly1305Key)) { throw new AuthenticityException(); } byte[] ret = new byte[box.length - OVERHEAD]; - for (int i = 0; i < ret.length; i++) { - ret[i] = box[i + OVERHEAD]; - } + System.arraycopy(box, 0 + OVERHEAD, ret, 0, ret.length); // We XOR up to 32 bytes of box with the keystream generated from // the first block. byte[] firstMessageBlock = new byte[ret.length]; if (ret.length > 32) { firstMessageBlock = new byte[32]; } - for (int i = 0; i < firstMessageBlock.length; i++) { - firstMessageBlock[i] = ret[i]; - } + System.arraycopy(ret, 0, firstMessageBlock, 0, firstMessageBlock.length); for (int i = 0; i < firstMessageBlock.length; i++) { ret[i] = (byte) (firstBlock[32 + i] ^ firstMessageBlock[i]); } @@ -101,9 +91,9 @@ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { byte[] rest = Salsa.XORKeyStream(newbox, counter, subKey); // Now decrypt the rest. - for (int i = firstMessageBlock.length; i < ret.length; i++) { - ret[i] = rest[i - firstMessageBlock.length]; - } + System.arraycopy(rest, 0, ret, firstMessageBlock.length, + ret.length - firstMessageBlock.length); + return ret; } @@ -122,16 +112,10 @@ private void setup(byte[] subKey, byte[] counter, byte[] nonce, byte[] key) { // We use XSalsa20 for encryption so first we need to generate a // key and nonce with HSalsa20. byte[] hNonce = new byte[16]; - for (int i = 0; i < hNonce.length; i++) { - hNonce[i] = nonce[i]; - } + System.arraycopy(nonce, 0, hNonce, 0, hNonce.length); byte[] newSubKey = Salsa.HSalsa20(hNonce, key, Salsa.SIGMA); - for (int i = 0; i < subKey.length; i++) { - subKey[i] = newSubKey[i]; - } + System.arraycopy(newSubKey, 0, subKey, 0, subKey.length); - for (int i = 0; i < nonce.length - 16; i++) { - counter[i] = nonce[i + 16]; - } + System.arraycopy(nonce, 16, counter, 0, nonce.length - 16); } } From 75143846aa8b3b3148b3887162e65dcb134fba61 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Tue, 31 Mar 2020 16:41:45 +0100 Subject: [PATCH 14/48] Add validation for nonce length --- .../java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index 686f3959..c13d466b 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -45,6 +45,8 @@ public SecretBoxOpener(byte[] key) { public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { checkNotNull(key, "key has been cleared, create new instance"); + checkArgument(nonce.length == 24, "nonce length must be 24 bytes, but is " + + key.length + " bytes"); byte[] subKey = new byte[32]; byte[] counter = new byte[16]; From d6b6bfd36baf8a75ffdfa2354d5217787c50b07d Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:15:58 +0100 Subject: [PATCH 15/48] Fix warning about redundant array init --- .../java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index c13d466b..a5c8cdd7 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -56,9 +56,6 @@ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { // Salsa20 works with 64-byte blocks, we also generate 32 bytes of // keystream as a side effect. byte[] firstBlock = new byte[64]; - for (int i = 0; i < firstBlock.length; i++) { - firstBlock[i] = 0; - } firstBlock = Salsa.XORKeyStream(firstBlock, counter, subKey); byte[] poly1305Key = new byte[32]; From 7763927ae4dd344c02290c29c27fa55fddb45cd3 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:18:28 +0100 Subject: [PATCH 16/48] Fix warnings about pointless operations --- src/main/java/com/pusher/client/crypto/nacl/Poly1305.java | 4 ++-- .../java/com/pusher/client/crypto/nacl/SecretBoxOpener.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index 3ac071c1..18d4afea 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -512,7 +512,7 @@ public static byte[] sum(byte[] m, byte[] key) { y0 -= alpha32; m2 += m23; - m00 = 0xFF & (m[(int) p + 0]); + m00 = 0xFF & (m[(int) p]); y5 = h5 + alpha96; m31 <<= 8; @@ -904,7 +904,7 @@ public static byte[] sum(byte[] m, byte[] key) { lbelow2 >>= 31; lbelow4 = l - 4; - m00 = 0xFF & (m[(int) p + 0]); + m00 = 0xFF & (m[(int) p]); lbelow3 >>= 31; p += lbelow2; diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index a5c8cdd7..ed85944c 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -64,13 +64,13 @@ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { System.arraycopy(box, 0, tag, 0, tag.length); byte[] cipher = new byte[box.length - Poly1305.TAG_SIZE]; - System.arraycopy(box, 0 + Poly1305.TAG_SIZE, cipher, 0, cipher.length); + System.arraycopy(box, Poly1305.TAG_SIZE, cipher, 0, cipher.length); if (!Poly1305.verify(tag, cipher, poly1305Key)) { throw new AuthenticityException(); } byte[] ret = new byte[box.length - OVERHEAD]; - System.arraycopy(box, 0 + OVERHEAD, ret, 0, ret.length); + System.arraycopy(box, OVERHEAD, ret, 0, ret.length); // We XOR up to 32 bytes of box with the keystream generated from // the first block. byte[] firstMessageBlock = new byte[ret.length]; From ccc26917b64ceecfd2c711f1b331f52f1cbe6fa0 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:23:07 +0100 Subject: [PATCH 17/48] Fix warning about unnecessary semicolon --- src/main/java/com/pusher/client/crypto/nacl/Poly1305.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index 18d4afea..f2d009a5 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -1098,7 +1098,6 @@ public static byte[] sum(byte[] m, byte[] key) { d3 = m3; z3 = Double.longBitsToDouble(d3); - ; z2 = Double.longBitsToDouble(d2); From 090025e64f7c37e14fdd1c0c3a798663b3c0dd98 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:24:46 +0100 Subject: [PATCH 18/48] Fix warnings about lowercase 'l' for long literals --- src/main/java/com/pusher/client/crypto/nacl/Poly1305.java | 2 +- src/main/java/com/pusher/client/crypto/nacl/Salsa.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index f2d009a5..775b6d66 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -1384,7 +1384,7 @@ public static byte[] sum(byte[] m, byte[] key) { f0 = d0; f1 = d1; - bits32 = 0xFFFFFFFFFFFFFFFFl; + bits32 = 0xFFFFFFFFFFFFFFFFL; f2 = d2; bits32 >>>= 32; diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java index 1b013409..758cabae 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java @@ -29,14 +29,14 @@ public class Salsa { private static int rounds = 20; private static long mask(byte x) { - return 0xFFl & x; + return 0xFFL & x; } // core applies the Salsa20 core function to 16-byte input in, 32-byte key k, // and 16-byte constant c, and puts the result into 64-byte array out. public static byte[] core(byte[] in, byte[] k, byte[] c) { byte[] out = new byte[64]; - long mask = 0xFFFFFFFFl; + long mask = 0xFFFFFFFFL; long j0 = mask & (mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24); long j1 = mask & (mask(k[0]) | mask(k[1]) << 8 | mask(k[2]) << 16 | mask(k[3]) << 24); @@ -311,7 +311,7 @@ public static byte[] HSalsa20(byte[] in, byte[] k, byte[] c) { long x14 = mask(k[28]) | mask(k[29]) << 8 | mask(k[30]) << 16 | mask(k[31]) << 24; long x15 = mask(c[12]) | mask(c[13]) << 8 | mask(c[14]) << 16 | mask(c[15]) << 24; - long mask = 0xFFFFFFFFl; + long mask = 0xFFFFFFFFL; for (int i = 0; i < 20; i += 2) { long u = mask & (x0 + x12); x4 ^= mask & (u << 7 | u >>> (32 - 7)); From bc366a8a67fed460a7334046add2ffb09f78f710 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 10:41:26 +0100 Subject: [PATCH 19/48] Make constants marked and named as such --- .../pusher/client/crypto/nacl/Poly1305.java | 273 +++++++++--------- .../com/pusher/client/crypto/nacl/Salsa.java | 6 +- 2 files changed, 140 insertions(+), 139 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java index 775b6d66..e77a0574 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java @@ -24,25 +24,25 @@ a copy of this software and associated documentation files (the "Software"), package com.pusher.client.crypto.nacl; public class Poly1305 { - public static int TAG_SIZE = 16; - - private static double alpham80 = 0.00000000558793544769287109375d; - private static double alpham48 = 24.0d; - private static double alpham16 = 103079215104.0d; - private static double alpha0 = 6755399441055744.0d; - private static double alpha18 = 1770887431076116955136.0d; - private static double alpha32 = 29014219670751100192948224.0d; - private static double alpha50 = 7605903601369376408980219232256.0d; - private static double alpha64 = 124615124604835863084731911901282304.0d; - private static double alpha82 = 32667107224410092492483962313449748299776.0d; - private static double alpha96 = 535217884764734955396857238543560676143529984.0d; - private static double alpha112 = 35076039295941670036888435985190792471742381031424.0d; - private static double alpha130 = 9194973245195333150150082162901855101712434733101613056.0d; - private static double scale = 0.0000000000000000000000000000000000000036734198463196484624023016788195177431833298649127735047148490821200539357960224151611328125d; - private static double offset0 = 6755408030990331.0d; - private static double offset1 = 29014256564239239022116864.0d; - private static double offset2 = 124615283061160854719918951570079744.0d; - private static double offset3 = 535219245894202480694386063513315216128475136.0d; + public static final int TAG_SIZE = 16; + + private static final double ALPHAM_80 = 0.00000000558793544769287109375d; + private static final double ALPHAM_48 = 24.0d; + private static final double ALPHAM_16 = 103079215104.0d; + private static final double ALPHA_0 = 6755399441055744.0d; + private static final double ALPHA_18 = 1770887431076116955136.0d; + private static final double ALPHA_32 = 29014219670751100192948224.0d; + private static final double ALPHA_50 = 7605903601369376408980219232256.0d; + private static final double ALPHA_64 = 124615124604835863084731911901282304.0d; + private static final double ALPHA_82 = 32667107224410092492483962313449748299776.0d; + private static final double ALPHA_96 = 535217884764734955396857238543560676143529984.0d; + private static final double ALPHA_112 = 35076039295941670036888435985190792471742381031424.0d; + private static final double ALPHA_130 = 9194973245195333150150082162901855101712434733101613056.0d; + private static final double SCALE = 0.0000000000000000000000000000000000000036734198463196484624023016788195177431833298649127735047148490821200539357960224151611328125d; + private static final double OFFSET_0 = 6755408030990331.0d; + private static final double OFFSET_1 = 29014256564239239022116864.0d; + private static final double OFFSET_2 = 124615283061160854719918951570079744.0d; + private static final double OFFSET_3 = 535219245894202480694386063513315216128475136.0d; private static long uint32(long x) { return 0xFFFFFFFF & x; @@ -67,6 +67,7 @@ public static boolean verify(byte[] mac, byte[] m, byte[] key) { // Sum generates an authenticator for m using a one-time key and puts the // 16-byte result into out. Authenticating two different messages with the same // key allows an attacker to forge messages at will. + @SuppressWarnings("SuspiciousNameCombination") public static byte[] sum(byte[] m, byte[] key) { byte[] r = key.clone(); byte[] s = new byte[16]; @@ -274,87 +275,87 @@ public static byte[] sum(byte[] m, byte[] key) { r3 += r32; r3 += r33; - double h0 = alpha32 - alpha32; + double h0 = ALPHA_32 - ALPHA_32; long d3 = r3; - double h1 = alpha32 - alpha32; + double h1 = ALPHA_32 - ALPHA_32; - double h2 = alpha32 - alpha32; + double h2 = ALPHA_32 - ALPHA_32; - double h3 = alpha32 - alpha32; + double h3 = ALPHA_32 - ALPHA_32; - double h4 = alpha32 - alpha32; + double h4 = ALPHA_32 - ALPHA_32; double r0low = Double.longBitsToDouble(d0); - double h5 = alpha32 - alpha32; + double h5 = ALPHA_32 - ALPHA_32; double r1low = longBitsToDouble(d1); - double h6 = alpha32 - alpha32; + double h6 = ALPHA_32 - ALPHA_32; double r2low = Double.longBitsToDouble(d2); - double h7 = alpha32 - alpha32; + double h7 = ALPHA_32 - ALPHA_32; - r0low -= alpha0; + r0low -= ALPHA_0; - r1low -= alpha32; + r1low -= ALPHA_32; - r2low -= alpha64; + r2low -= ALPHA_64; - double r0high = r0low + alpha18; + double r0high = r0low + ALPHA_18; double r3low = Double.longBitsToDouble(d3); - double r1high = r1low + alpha50; - double sr1low = scale * r1low; + double r1high = r1low + ALPHA_50; + double sr1low = SCALE * r1low; - double r2high = r2low + alpha82; - double sr2low = scale * r2low; + double r2high = r2low + ALPHA_82; + double sr2low = SCALE * r2low; - r0high -= alpha18; + r0high -= ALPHA_18; double r0high_stack = r0high; - r3low -= alpha96; + r3low -= ALPHA_96; - r1high -= alpha50; + r1high -= ALPHA_50; double r1high_stack = r1high; - double sr1high = sr1low + alpham80; + double sr1high = sr1low + ALPHAM_80; r0low -= r0high; - r2high -= alpha82; - sr3low = scale * r3low; + r2high -= ALPHA_82; + sr3low = SCALE * r3low; - double sr2high = sr2low + alpham48; + double sr2high = sr2low + ALPHAM_48; r1low -= r1high; double r1low_stack = r1low; - sr1high -= alpham80; + sr1high -= ALPHAM_80; double sr1high_stack = sr1high; r2low -= r2high; double r2low_stack = r2low; - sr2high -= alpham48; + sr2high -= ALPHAM_48; double sr2high_stack = sr2high; - double r3high = r3low + alpha112; + double r3high = r3low + ALPHA_112; double r0low_stack = r0low; sr1low -= sr1high; double sr1low_stack = sr1low; - double sr3high = sr3low + alpham16; + double sr3high = sr3low + ALPHAM_16; double r2high_stack = r2high; sr2low -= sr2high; double sr2low_stack = sr2low; - r3high -= alpha112; + r3high -= ALPHA_112; double r3high_stack = r3high; - sr3high -= alpham16; + sr3high -= ALPHAM_16; double sr3high_stack = sr3high; r3low -= r3high; @@ -454,13 +455,13 @@ public static byte[] sum(byte[] m, byte[] key) { z3 = Double.longBitsToDouble(d3); - z0 -= alpha0; + z0 -= ALPHA_0; - z1 -= alpha32; + z1 -= ALPHA_32; - z2 -= alpha64; + z2 -= ALPHA_64; - z3 -= alpha96; + z3 -= ALPHA_96; h0 += z0; @@ -475,60 +476,60 @@ public static byte[] sum(byte[] m, byte[] key) { m2 = 2279; m20 = 0xFF & (m[(int) p + 8]); - y7 = h7 + alpha130; + y7 = h7 + ALPHA_130; m2 <<= 51; m3 = 2343; m21 = 0xFF & (m[(int) p + 9]); - y6 = h6 + alpha130; + y6 = h6 + ALPHA_130; m3 <<= 51; m0 = 2151; m22 = 0xFF & (m[(int) p + 10]); - y1 = h1 + alpha32; + y1 = h1 + ALPHA_32; m0 <<= 51; m1 = 2215; m23 = 0xFF & (m[(int) p + 11]); - y0 = h0 + alpha32; + y0 = h0 + ALPHA_32; m1 <<= 51; m30 = 0xFF & (m[(int) p + 12]); - y7 -= alpha130; + y7 -= ALPHA_130; m21 <<= 8; m2 += m20; m31 = 0xFF & (m[(int) p + 13]); - y6 -= alpha130; + y6 -= ALPHA_130; m22 <<= 16; m2 += m21; m32 = 0xFF & (m[(int) p + 14]); - y1 -= alpha32; + y1 -= ALPHA_32; m23 <<= 24; m2 += m22; m33 = 0xFF & (m[(int) p + 15]); - y0 -= alpha32; + y0 -= ALPHA_32; m2 += m23; m00 = 0xFF & (m[(int) p]); - y5 = h5 + alpha96; + y5 = h5 + ALPHA_96; m31 <<= 8; m3 += m30; m01 = 0xFF & (m[(int) p + 1]); - y4 = h4 + alpha96; + y4 = h4 + ALPHA_96; m32 <<= 16; m02 = 0xFF & (m[(int) p + 2]); x7 = h7 - y7; - y7 *= scale; + y7 *= SCALE; m33 += 256; m03 = 0xFF & (m[(int) p + 3]); x6 = h6 - y6; - y6 *= scale; + y6 *= SCALE; m33 <<= 24; m3 += m31; @@ -543,12 +544,12 @@ public static byte[] sum(byte[] m, byte[] key) { m3 += m33; m0 += m00; m12 = 0xFF & (m[(int) p + 6]); - y5 -= alpha96; + y5 -= ALPHA_96; m02 <<= 16; m0 += m01; m13 = 0xFF & (m[(int) p + 7]); - y4 -= alpha96; + y4 -= ALPHA_96; m03 <<= 24; m0 += m02; @@ -570,20 +571,20 @@ public static byte[] sum(byte[] m, byte[] key) { m13 <<= 24; m1 += m12; - y3 = h3 + alpha64; + y3 = h3 + ALPHA_64; m1 += m13; d1 = m1; - y2 = h2 + alpha64; + y2 = h2 + ALPHA_64; x0 += x1; x6 += x7; - y3 -= alpha64; + y3 -= ALPHA_64; r3low = r3low_stack; - y2 -= alpha64; + y2 -= ALPHA_64; r0low = r0low_stack; x5 = h5 - y5; @@ -649,13 +650,13 @@ public static byte[] sum(byte[] m, byte[] key) { sr3highx6 = sr3high * x6; r1highx4 = r1high * x4; - z2 -= alpha64; + z2 -= ALPHA_64; h4 = r2lowx0 + sr3lowx6; r1lowx4 = r1low * x4; r0highx4 = r0high * x4; - z3 -= alpha96; + z3 -= ALPHA_96; h5 = r2highx0 + sr3highx6; r0lowx4 = r0low * x4; @@ -708,9 +709,9 @@ public static byte[] sum(byte[] m, byte[] key) { z0 = Double.longBitsToDouble(d0); h1 += sr3highx2; - z1 -= alpha32; + z1 -= ALPHA_32; - z0 -= alpha0; + z0 -= ALPHA_0; h5 += z3; @@ -723,39 +724,39 @@ public static byte[] sum(byte[] m, byte[] key) { } // multiplyaddatmost15bytes: - y7 = h7 + alpha130; + y7 = h7 + ALPHA_130; - y6 = h6 + alpha130; + y6 = h6 + ALPHA_130; - y1 = h1 + alpha32; + y1 = h1 + ALPHA_32; - y0 = h0 + alpha32; + y0 = h0 + ALPHA_32; - y7 -= alpha130; + y7 -= ALPHA_130; - y6 -= alpha130; + y6 -= ALPHA_130; - y1 -= alpha32; + y1 -= ALPHA_32; - y0 -= alpha32; + y0 -= ALPHA_32; - y5 = h5 + alpha96; + y5 = h5 + ALPHA_96; - y4 = h4 + alpha96; + y4 = h4 + ALPHA_96; x7 = h7 - y7; - y7 *= scale; + y7 *= SCALE; x6 = h6 - y6; - y6 *= scale; + y6 *= SCALE; x1 = h1 - y1; x0 = h0 - y0; - y5 -= alpha96; + y5 -= ALPHA_96; - y4 -= alpha96; + y4 -= ALPHA_96; x1 += y7; @@ -765,18 +766,18 @@ public static byte[] sum(byte[] m, byte[] key) { x6 += y4; - y3 = h3 + alpha64; + y3 = h3 + ALPHA_64; - y2 = h2 + alpha64; + y2 = h2 + ALPHA_64; x0 += x1; x6 += x7; - y3 -= alpha64; + y3 -= ALPHA_64; r3low = r3low_stack; - y2 -= alpha64; + y2 -= ALPHA_64; r0low = r0low_stack; x5 = h5 - y5; @@ -1105,13 +1106,13 @@ public static byte[] sum(byte[] m, byte[] key) { z0 = Double.longBitsToDouble(d0); - z3 -= alpha96; + z3 -= ALPHA_96; - z2 -= alpha64; + z2 -= ALPHA_64; - z1 -= alpha32; + z1 -= ALPHA_32; - z0 -= alpha0; + z0 -= ALPHA_0; h5 += z3; @@ -1121,39 +1122,39 @@ public static byte[] sum(byte[] m, byte[] key) { h0 += z0; - y7 = h7 + alpha130; + y7 = h7 + ALPHA_130; - y6 = h6 + alpha130; + y6 = h6 + ALPHA_130; - y1 = h1 + alpha32; + y1 = h1 + ALPHA_32; - y0 = h0 + alpha32; + y0 = h0 + ALPHA_32; - y7 -= alpha130; + y7 -= ALPHA_130; - y6 -= alpha130; + y6 -= ALPHA_130; - y1 -= alpha32; + y1 -= ALPHA_32; - y0 -= alpha32; + y0 -= ALPHA_32; - y5 = h5 + alpha96; + y5 = h5 + ALPHA_96; - y4 = h4 + alpha96; + y4 = h4 + ALPHA_96; x7 = h7 - y7; - y7 *= scale; + y7 *= SCALE; x6 = h6 - y6; - y6 *= scale; + y6 *= SCALE; x1 = h1 - y1; x0 = h0 - y0; - y5 -= alpha96; + y5 -= ALPHA_96; - y4 -= alpha96; + y4 -= ALPHA_96; x1 += y7; @@ -1163,18 +1164,18 @@ public static byte[] sum(byte[] m, byte[] key) { x6 += y4; - y3 = h3 + alpha64; + y3 = h3 + ALPHA_64; - y2 = h2 + alpha64; + y2 = h2 + ALPHA_64; x0 += x1; x6 += x7; - y3 -= alpha64; + y3 -= ALPHA_64; r3low = r3low_stack; - y2 -= alpha64; + y2 -= ALPHA_64; r0low = r0low_stack; x5 = h5 - y5; @@ -1294,40 +1295,40 @@ public static byte[] sum(byte[] m, byte[] key) { //nomorebytes: - y7 = h7 + alpha130; + y7 = h7 + ALPHA_130; - y0 = h0 + alpha32; + y0 = h0 + ALPHA_32; - y1 = h1 + alpha32; + y1 = h1 + ALPHA_32; - y2 = h2 + alpha64; + y2 = h2 + ALPHA_64; - y7 -= alpha130; + y7 -= ALPHA_130; - y3 = h3 + alpha64; + y3 = h3 + ALPHA_64; - y4 = h4 + alpha96; + y4 = h4 + ALPHA_96; - y5 = h5 + alpha96; + y5 = h5 + ALPHA_96; x7 = h7 - y7; - y7 *= scale; + y7 *= SCALE; - y0 -= alpha32; + y0 -= ALPHA_32; - y1 -= alpha32; + y1 -= ALPHA_32; - y2 -= alpha64; + y2 -= ALPHA_64; h6 += x7; - y3 -= alpha64; + y3 -= ALPHA_64; - y4 -= alpha96; + y4 -= ALPHA_96; - y5 -= alpha96; + y5 -= ALPHA_96; - y6 = h6 + alpha130; + y6 = h6 + ALPHA_130; x0 = h0 - y0; @@ -1335,7 +1336,7 @@ public static byte[] sum(byte[] m, byte[] key) { x2 = h2 - y2; - y6 -= alpha130; + y6 -= ALPHA_130; x0 += y7; @@ -1347,7 +1348,7 @@ public static byte[] sum(byte[] m, byte[] key) { x6 = h6 - y6; - y6 *= scale; + y6 *= SCALE; x2 += y0; @@ -1369,16 +1370,16 @@ public static byte[] sum(byte[] m, byte[] key) { x6 += y5; - x2 += offset1; + x2 += OFFSET_1; d1 = Double.doubleToLongBits(x2); - x0 += offset0; + x0 += OFFSET_0; d0 = Double.doubleToLongBits(x0); - x4 += offset2; + x4 += OFFSET_2; d2 = Double.doubleToLongBits(x4); - x6 += offset3; + x6 += OFFSET_3; d3 = Double.doubleToLongBits(x6); f0 = d0; diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java index 758cabae..873ebd83 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java +++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java @@ -24,9 +24,9 @@ a copy of this software and associated documentation files (the "Software"), package com.pusher.client.crypto.nacl; public class Salsa { - public static byte[] SIGMA = {'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'}; + public static final byte[] SIGMA = {'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'}; - private static int rounds = 20; + private static final int ROUNDS = 20; private static long mask(byte x) { return 0xFFL & x; @@ -60,7 +60,7 @@ public static byte[] core(byte[] in, byte[] k, byte[] c) { long x9 = j9, x10 = j10, x11 = j11, x12 = j12; long x13 = j13, x14 = j14, x15 = j15; - for (int i = 0; i < rounds; i += 2) { + for (int i = 0; i < ROUNDS; i += 2) { long u = mask & (x0 + x12); x4 ^= mask & (u << 7 | u >>> (32 - 7)); u = mask & (x4 + x0); From 36747fd7ac5e2a2d2dbf0d8a3452fda283f7c4f4 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Wed, 1 Apr 2020 16:39:07 +0100 Subject: [PATCH 20/48] Private Encrypted Channel (#234) * Add a PrivateEncryptedChannel * Add an example PrivateEncryptedChannel app * Ensure the PrivateEncryptedChannelImpl prepares itself before subscribing * Add tests for the PrivateEncryptedChannelImpl prepare method * Save PrivateEncryptedChannel shared secret to the secretbox and clear when unsubscribing * PrivateEncryptedChannelImpl handle sharedSecret better * Save the auth token more securely in the PrivateEncryptedChannelImpl * Remove empty line in ChannelManager * Use Marek's base64 decode to decode the shared secret * Make the PrivateEncryptedChannelImpl error message more clear * Fix PrivateEncryptedChannelImpl raising the correct exceptions * Update the PrivateEncryptedChannelImplTest to check we're raising the correct exceptions * Add space character in Pusher.java when creating a PrivateEncryptedChannelImpl * Keep the auth token in an byte array for as long as possible in PrivateEncryptedChannelImpl * Update src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java Co-Authored-By: Marek <8502071+marekoid@users.noreply.github.com> * Update src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java Co-Authored-By: Marek <8502071+marekoid@users.noreply.github.com> * Refactor new pieces to fit 100 characters maxline length better * Remove rogue space character in PrivateEncrptedChanneImplTest * Remove PrivateEncryptedChannel custom method calls from the ChannelManager * Make the PrivateEncryptedChannelEventListener extend PrivateChannelEventListener * Refactor clearDownSubscription to handleAuthenticationFailure * Update src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java Co-Authored-By: Marek <8502071+marekoid@users.noreply.github.com> * Refactor PrivateEncryptedChannelImpl prepareChannel to checkAuthentication * Refactor PrivateEncryptedChannelImplTest * Refactor PrivateEncryptedChannelImplTest titles and valid authorizer * Import Base64 util in PrivateEncryptedChannelImpl properly * Refactor Arrays.fill to fill for readability improvement * Refactor PrivateEncryptedChannelImpl to remove channelData * Don't log any secret info * Refactor unit tests for PrivateEncryptedChannelImpl to assert throws errors differently * Add private-encrypted channel name tests * Make the PrivateEncryptedChannelImplTests extend the ChannelImplTest and implement overrides * Fix PrivateEncryptedChannelImplTest for testReturnsCorrectSubscribeMessage * Fix PrivateEncryptedChannelImplTests for unsubscribing * Add Tests for SecretBoxOpener * Update AuthorizationFailureException in PrivateEncryptedChannelImpl * Update src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java Co-Authored-By: Marek <8502071+marekoid@users.noreply.github.com> * wip for gson deserialising * Put the PrivateEncryptedChannelImpl authoriser data back to String * Add dash to the end of ChannelManager to check for private-encrypted- * Add dash to the end of ChannelManager to check for private-encrypted- * Remove the demo apikey * Ensure getDisallowedNameExpression ends with dash * Remove unused imports and comments * Rename checkAuthentication to authenticate and make private * Use auth consistently * Add SecretBoxOpenerFactory * Make auth a String Co-authored-by: Marek <8502071+marekoid@users.noreply.github.com> --- src/main/java/com/pusher/client/Pusher.java | 45 +++++ .../channel/PrivateChannelEventListener.java | 6 +- .../channel/PrivateEncryptedChannel.java | 9 + .../PrivateEncryptedChannelEventListener.java | 10 + .../client/channel/impl/ChannelImpl.java | 2 +- .../client/channel/impl/ChannelManager.java | 13 +- .../channel/impl/PrivateChannelImpl.java | 5 +- .../impl/PrivateEncryptedChannelImpl.java | 119 +++++++++++ .../crypto/nacl/SecretBoxOpenerFactory.java | 8 + .../PrivateEncryptedChannelExampleApp.java | 90 +++++++++ .../java/com/pusher/client/util/Factory.java | 10 + .../client/channel/impl/ChannelImplTest.java | 5 + .../channel/impl/PresenceChannelImplTest.java | 6 + .../channel/impl/PrivateChannelImplTest.java | 6 + .../impl/PrivateEncryptedChannelImplTest.java | 189 ++++++++++++++++++ 15 files changed, 515 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java create mode 100644 src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java create mode 100644 src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java create mode 100644 src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java create mode 100644 src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java create mode 100644 src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java diff --git a/src/main/java/com/pusher/client/Pusher.java b/src/main/java/com/pusher/client/Pusher.java index b0dfec03..34746c8a 100644 --- a/src/main/java/com/pusher/client/Pusher.java +++ b/src/main/java/com/pusher/client/Pusher.java @@ -2,6 +2,8 @@ import com.pusher.client.channel.Channel; import com.pusher.client.channel.ChannelEventListener; +import com.pusher.client.channel.PrivateEncryptedChannel; +import com.pusher.client.channel.PrivateEncryptedChannelEventListener; import com.pusher.client.channel.PresenceChannel; import com.pusher.client.channel.PresenceChannelEventListener; import com.pusher.client.channel.PrivateChannel; @@ -11,6 +13,7 @@ import com.pusher.client.channel.impl.InternalChannel; import com.pusher.client.channel.impl.PresenceChannelImpl; import com.pusher.client.channel.impl.PrivateChannelImpl; +import com.pusher.client.channel.impl.PrivateEncryptedChannelImpl; import com.pusher.client.connection.Connection; import com.pusher.client.connection.ConnectionEventListener; import com.pusher.client.connection.ConnectionState; @@ -284,6 +287,38 @@ public PrivateChannel subscribePrivate(final String channelName, final PrivateCh return channel; } + + /** + * Subscribes to a {@link com.pusher.client.channel.PrivateEncryptedChannel} which + * requires authentication. + * + * @param channelName The name of the channel to subscribe to. + * @param listener A listener to be informed of both Pusher channel protocol events and + * subscription data events. + * @param eventNames An optional list of names of events to be bound to on the channel. + * The equivalent of calling + * {@link com.pusher.client.channel.Channel#bind(String, SubscriptionEventListener)} + * one or more times. + * @return A new {@link com.pusher.client.channel.PrivateEncryptedChannel} representing + * the subscription. + * @throws IllegalStateException if a {@link com.pusher.client.Authorizer} has not been set for + * the {@link Pusher} instance via {@link #Pusher(String, PusherOptions)}. + */ + public PrivateEncryptedChannel subscribePrivateEncrypted( + final String channelName, + final PrivateEncryptedChannelEventListener listener, + final String... eventNames) { + + throwExceptionIfNoAuthorizerHasBeenSet(); + + final PrivateEncryptedChannelImpl channel = factory.newPrivateEncryptedChannel( + connection, channelName, pusherOptions.getAuthorizer()); + channelManager.subscribeTo(channel, listener, eventNames); + + return channel; + } + + /** * Subscribes to a {@link com.pusher.client.channel.PresenceChannel} which * requires authentication. @@ -363,6 +398,16 @@ public PrivateChannel getPrivateChannel(String channelName){ return channelManager.getPrivateChannel(channelName); } + /** + * + * @param channelName The name of the private encrypted channel to be retrieved + * @return A private encrypted channel, or null if it could not be found + * @throws IllegalArgumentException if you try to retrieve a public or presence channel. + */ + public PrivateEncryptedChannel getPrivateEncryptedChannel(String channelName){ + return channelManager.getPrivateEncryptedChannel(channelName); + } + /** * * @param channelName The name of the presence channel to be retrieved diff --git a/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java index 9a2da678..7bbc7d0b 100644 --- a/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java +++ b/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java @@ -7,10 +7,8 @@ public interface PrivateChannelEventListener extends ChannelEventListener { /** * Called when an attempt to authenticate a private channel fails. * - * @param message - * A description of the problem. - * @param e - * An associated exception, if available. + * @param message A description of the problem. + * @param e An associated exception, if available. */ void onAuthenticationFailure(String message, Exception e); } diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java new file mode 100644 index 00000000..98f29dfe --- /dev/null +++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java @@ -0,0 +1,9 @@ +package com.pusher.client.channel; + +/** + * Represents a subscription to an encrypted private channel. + */ +public interface PrivateEncryptedChannel extends Channel { + + // it's not currently possible to send a message using private encrypted channels +} diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java new file mode 100644 index 00000000..a6aeeaae --- /dev/null +++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java @@ -0,0 +1,10 @@ +package com.pusher.client.channel; + +/** + * Interface to listen to private encrypted channel events. + * Note: This needs to extend the PrivateChannelEventListener because of the ChannelManager clearDownSubscription + */ +public interface PrivateEncryptedChannelEventListener extends PrivateChannelEventListener { + + // TODO: add onDecryptionFailure +} diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java index 39783c4b..4ae849b6 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java @@ -13,7 +13,7 @@ import com.pusher.client.util.Factory; public class ChannelImpl implements InternalChannel { - private final Gson GSON; + protected final Gson GSON; private static final String INTERNAL_EVENT_PREFIX = "pusher_internal:"; protected static final String SUBSCRIPTION_SUCCESS_EVENT = "pusher_internal:subscription_succeeded"; protected final String name; diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelManager.java b/src/main/java/com/pusher/client/channel/impl/ChannelManager.java index 08a019ae..bc6cf268 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelManager.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelManager.java @@ -8,6 +8,7 @@ import com.pusher.client.channel.Channel; import com.pusher.client.channel.ChannelEventListener; import com.pusher.client.channel.ChannelState; +import com.pusher.client.channel.PrivateEncryptedChannel; import com.pusher.client.channel.PresenceChannel; import com.pusher.client.channel.PrivateChannel; import com.pusher.client.channel.PrivateChannelEventListener; @@ -46,6 +47,14 @@ public PrivateChannel getPrivateChannel(String channelName) throws IllegalArgume } } + public PrivateEncryptedChannel getPrivateEncryptedChannel(String channelName) throws IllegalArgumentException{ + if (!channelName.startsWith("private-encrypted-")) { + throw new IllegalArgumentException("Encrypted private channels must begin with 'private-encrypted-'"); + } else { + return (PrivateEncryptedChannel) findChannelInChannelMap(channelName); + } + } + public PresenceChannel getPresenceChannel(String channelName) throws IllegalArgumentException{ if (!channelName.startsWith("presence-")) { throw new IllegalArgumentException("Presence channels must begin with 'presence-'"); @@ -141,7 +150,7 @@ public void run() { connection.sendMessage(message); channel.updateState(ChannelState.SUBSCRIBE_SENT); } catch (final AuthorizationFailureException e) { - clearDownSubscription(channel, e); + handleAuthenticationFailure(channel, e); } } } @@ -158,7 +167,7 @@ public void run() { }); } - private void clearDownSubscription(final InternalChannel channel, final Exception e) { + private void handleAuthenticationFailure(final InternalChannel channel, final Exception e) { channelNameToChannelMap.remove(channel.getName()); channel.updateState(ChannelState.FAILED); diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java index 5260fdf6..e6c27a54 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java @@ -116,7 +116,10 @@ public String toSubscribeMessage() { @Override protected String[] getDisallowedNameExpressions() { - return new String[] { "^(?!private-).*" }; + return new String[] { + "^(?!private-).*", // double negative, don't not start with private- + "^private-encrypted-.*" // doesn't start with private-encrypted- + }; } /** diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java new file mode 100644 index 00000000..05c46ba5 --- /dev/null +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -0,0 +1,119 @@ +package com.pusher.client.channel.impl; + +import com.pusher.client.AuthorizationFailureException; +import com.pusher.client.Authorizer; +import com.pusher.client.channel.ChannelState; +import com.pusher.client.channel.PrivateEncryptedChannel; +import com.pusher.client.channel.PrivateEncryptedChannelEventListener; +import com.pusher.client.channel.SubscriptionEventListener; +import com.pusher.client.connection.impl.InternalConnection; +import com.pusher.client.crypto.nacl.SecretBoxOpener; +import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; +import com.pusher.client.util.Factory; +import com.pusher.client.util.internal.Base64; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateEncryptedChannel { + + private final InternalConnection connection; + private final Authorizer authorizer; + private SecretBoxOpenerFactory secretBoxOpenerFactory; + private SecretBoxOpener secretBoxOpener; + + public PrivateEncryptedChannelImpl(final InternalConnection connection, + final String channelName, + final Authorizer authorizer, + final Factory factory, + final SecretBoxOpenerFactory secretBoxOpenerFactory) { + super(channelName, factory); + this.connection = connection; + this.authorizer = authorizer; + this.secretBoxOpenerFactory = secretBoxOpenerFactory; + } + + @Override + public void bind(final String eventName, final SubscriptionEventListener listener) { + + if (!(listener instanceof PrivateEncryptedChannelEventListener)) { + throw new IllegalArgumentException( + "Only instances of PrivateEncryptedChannelEventListener can be bound " + + "to a private encrypted channel"); + } + + super.bind(eventName, listener); + } + + private String authenticate() { + + try { + final Map authResponseMap = GSON.fromJson(getAuthResponse(), Map.class); + final String auth = (String) authResponseMap.get("auth"); + final String sharedSecret = (String) authResponseMap.get("shared_secret"); + + if (auth == null || sharedSecret == null) { + throw new AuthorizationFailureException("Didn't receive all the fields expected " + + "from the Authorizer, expected an auth and shared_secret."); + } else { + secretBoxOpener = secretBoxOpenerFactory.create( + Base64.decode(sharedSecret)); + return auth; + } + + } catch (final AuthorizationFailureException e) { + throw e; // pass this upwards + } catch (final Exception e) { + // any other errors need to be captured properly and passed upwards + throw new AuthorizationFailureException("Unable to parse response from Authorizer", e); + } + } + + @Override + public String toSubscribeMessage() { + + String authKey = authenticate(); + + // create the data part + final Map dataMap = new LinkedHashMap(); + dataMap.put("channel", name); + dataMap.put("auth", authKey); + + // create the wrapper part + final Map jsonObject = new LinkedHashMap(); + jsonObject.put("event", "pusher:subscribe"); + jsonObject.put("data", dataMap); + + return GSON.toJson(jsonObject); + } + + @Override + public void updateState(ChannelState state) { + super.updateState(state); + + if (state == ChannelState.UNSUBSCRIBED) { + tearDownChannel(); + } + } + + private void tearDownChannel() { + if (secretBoxOpener != null) { + secretBoxOpener.clearKey(); + } + } + + private String getAuthResponse() { + final String socketId = connection.getSocketId(); + return authorizer.authorize(getName(), socketId); + } + + @Override + protected String[] getDisallowedNameExpressions() { + return new String[] { "^(?!private-encrypted-).*" }; + } + + @Override + public String toString() { + return String.format("[Private Encrypted Channel: name=%s]", name); + } +} diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java new file mode 100644 index 00000000..ea644c85 --- /dev/null +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java @@ -0,0 +1,8 @@ +package com.pusher.client.crypto.nacl; + +public class SecretBoxOpenerFactory { + + public SecretBoxOpener create(byte[] key) { + return new SecretBoxOpener(key); + } +} diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java new file mode 100644 index 00000000..340fdb57 --- /dev/null +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -0,0 +1,90 @@ +package com.pusher.client.example; + +import com.pusher.client.Pusher; +import com.pusher.client.PusherOptions; +import com.pusher.client.channel.PrivateEncryptedChannel; +import com.pusher.client.channel.PrivateEncryptedChannelEventListener; +import com.pusher.client.channel.PusherEvent; +import com.pusher.client.connection.ConnectionEventListener; +import com.pusher.client.connection.ConnectionStateChange; +import com.pusher.client.util.HttpAuthorizer; + +public class PrivateEncryptedChannelExampleApp implements + ConnectionEventListener, PrivateEncryptedChannelEventListener { + + private String apiKey = "FILL_ME_IN"; + private String channelName = "private-encrypted-channel"; + private String eventName = "my-event"; + private String cluster = "eu"; + + private final PrivateEncryptedChannel channel; + + public static void main(final String[] args) { + new PrivateEncryptedChannelExampleApp(args); + } + + private PrivateEncryptedChannelExampleApp(final String[] args) { + + if (args.length == 3) { + apiKey = args[0]; + channelName = args[1]; + eventName = args[2]; + cluster = args[3]; + } + + final HttpAuthorizer authorizer = new HttpAuthorizer( + "http://localhost:3030/pusher/auth"); + final PusherOptions options = new PusherOptions().setAuthorizer(authorizer).setEncrypted(true); + options.setCluster(cluster); + + Pusher pusher = new Pusher(apiKey, options); + pusher.connect(this); + + channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); + + // Keep main thread asleep while we watch for events or application will terminate + while (true) { + try { + Thread.sleep(1000); + } + catch (final InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onAuthenticationFailure(String message, Exception e) { + System.out.println(String.format( + "Authentication failure due to [%s], exception was [%s]", message, e)); + } + + @Override + public void onSubscriptionSucceeded(String channelName) { + System.out.println(String.format( + "Subscription to channel [%s] succeeded", channel.getName())); + } + + @Override + public void onEvent(PusherEvent event) { + System.out.println(String.format( + "Received event [%s]", event.toString())); + } + + @Override + public void onConnectionStateChange(ConnectionStateChange change) { + System.out.println(String.format( + "Connection state changed from [%s] to [%s]", + change.getPreviousState(), + change.getCurrentState())); + } + + @Override + public void onError(String message, String code, Exception e) { + System.out.println(String.format( + "An error was received with message [%s], code [%s], exception [%s]", + message, + code, + e)); + } +} diff --git a/src/main/java/com/pusher/client/util/Factory.java b/src/main/java/com/pusher/client/util/Factory.java index b7622c78..fd296380 100644 --- a/src/main/java/com/pusher/client/util/Factory.java +++ b/src/main/java/com/pusher/client/util/Factory.java @@ -14,8 +14,10 @@ import com.pusher.client.PusherOptions; import com.pusher.client.channel.impl.ChannelImpl; import com.pusher.client.channel.impl.ChannelManager; +import com.pusher.client.channel.impl.PrivateEncryptedChannelImpl; import com.pusher.client.channel.impl.PresenceChannelImpl; import com.pusher.client.channel.impl.PrivateChannelImpl; +import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; import com.pusher.client.connection.impl.InternalConnection; import com.pusher.client.connection.websocket.WebSocketClientWrapper; import com.pusher.client.connection.websocket.WebSocketConnection; @@ -86,6 +88,14 @@ public PrivateChannelImpl newPrivateChannel(final InternalConnection connection, return new PrivateChannelImpl(connection, channelName, authorizer, this); } + public PrivateEncryptedChannelImpl newPrivateEncryptedChannel( + final InternalConnection connection, + final String channelName, + final Authorizer authorizer) { + return new PrivateEncryptedChannelImpl(connection, channelName, authorizer, this, + new SecretBoxOpenerFactory()); + } + public PresenceChannelImpl newPresenceChannel(final InternalConnection connection, final String channelName, final Authorizer authorizer) { return new PresenceChannelImpl(connection, channelName, authorizer, this); diff --git a/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java index 917d3e41..894c2c01 100644 --- a/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java @@ -57,6 +57,11 @@ public void testPrivateChannelName() { newInstance("private-my-channel"); } + @Test(expected = IllegalArgumentException.class) + public void testPrivateEncryptedChannelName() { + newInstance("private-encrypted-my-channel"); + } + @Test(expected = IllegalArgumentException.class) public void testPresenceChannelName() { newInstance("presence-my-channel"); diff --git a/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java index 177e7367..a9c3f96e 100644 --- a/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java @@ -214,6 +214,12 @@ public void testPrivateChannelName() { newInstance("private-stuffchannel"); } + @Override + @Test(expected = IllegalArgumentException.class) + public void testPrivateEncryptedChannelName() { + newInstance("private-encrypted-stuffchannel"); + } + @Override @Test public void testPresenceChannelName() { diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java index 1bf0e6e7..16747677 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java @@ -64,6 +64,12 @@ public void testPresenceChannelName() { newInstance("presence-stuffchannel"); } + @Override + @Test(expected = IllegalArgumentException.class) + public void testPrivateEncryptedChannelName() { + newInstance("private-encrypted-stuffchannel"); + } + @Override @Test public void testPrivateChannelName() { diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java new file mode 100644 index 00000000..025872b5 --- /dev/null +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -0,0 +1,189 @@ +package com.pusher.client.channel.impl; + +import com.pusher.client.AuthorizationFailureException; +import com.pusher.client.Authorizer; +import com.pusher.client.channel.ChannelEventListener; +import com.pusher.client.channel.ChannelState; +import com.pusher.client.channel.PrivateEncryptedChannelEventListener; +import com.pusher.client.connection.impl.InternalConnection; +import com.pusher.client.crypto.nacl.SecretBoxOpener; +import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; +import com.pusher.client.util.Factory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PrivateEncryptedChannelImplTest extends ChannelImplTest { + + String authorizer_valid = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; + String authorizer_missingAuthKey = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; + String authorizer_missingSharedSecret = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; + String authorizer_malformedJson = "potatoes"; + + + @Mock + InternalConnection mockInternalConnection; + @Mock + Authorizer mockAuthorizer; + @Mock + Factory mockFactory; + @Mock + SecretBoxOpenerFactory mockSecretBoxOpenerFactory; + + @Mock + SecretBoxOpener mockSecretBoxOpener; + + @Override + @Before + public void setUp() { + super.setUp(); + when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(authorizer_valid); + } + + @Override + protected ChannelImpl newInstance(final String channelName) { + return new PrivateEncryptedChannelImpl(mockInternalConnection, channelName, mockAuthorizer, + factory, mockSecretBoxOpenerFactory); + } + + protected String getChannelName() { + return "private-encrypted-channel"; + } + + @Test + public void toStringIsAccurate() { + assertEquals("[Private Encrypted Channel: name="+getChannelName()+"]", channel.toString()); + } + + + /* + TESTING VALID PRIVATE ENCRYPTED CHANNEL NAMES + */ + + @Override + @Test(expected = IllegalArgumentException.class) + public void testPublicChannelName() { + newInstance("stuffchannel"); + } + + @Override + @Test(expected = IllegalArgumentException.class) + public void testPresenceChannelName() { + newInstance("presence-stuffchannel"); + } + + @Override + @Test + public void testPrivateEncryptedChannelName() { + newInstance("private-encrypted-stuffchannel"); + } + + @Override + @Test(expected = IllegalArgumentException.class) + public void testPrivateChannelName() { newInstance("private-stuffchannel"); } + + /* + TESTING SUBSCRIBE MESSAGE + */ + + @Override + @Test + public void testReturnsCorrectSubscribeMessage() { + assertEquals("{\"event\":\"pusher:subscribe\",\"data\":{" + + "\"channel\":\"" + getChannelName() + "\"," + + "\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\""+ + "}}", channel.toSubscribeMessage()); + } + + /* + TESTING AUTHENTICATION METHOD + */ + + @Test + public void authenticationSucceedsGivenValidAuthorizer() { + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(authorizer_valid); + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, + mockSecretBoxOpenerFactory); + + channel.toSubscribeMessage(); + } + + protected ChannelEventListener getEventListener() { + return mock(PrivateEncryptedChannelEventListener.class); + } + + @Test(expected = AuthorizationFailureException.class) + public void authenticationThrowsExceptionIfNoAuthKey() { + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(authorizer_missingAuthKey); + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, + mockSecretBoxOpenerFactory); + + channel.toSubscribeMessage(); + } + + @Test(expected = AuthorizationFailureException.class) + public void authenticationThrowsExceptionIfNoSharedSecret() { + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(authorizer_missingSharedSecret); + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, + mockSecretBoxOpenerFactory); + + channel.toSubscribeMessage(); + } + + @Test(expected = AuthorizationFailureException.class) + public void authenticationThrowsExceptionIfMalformedJson() { + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(authorizer_malformedJson); + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, + mockSecretBoxOpenerFactory); + + channel.toSubscribeMessage(); + } + + /* + TESTING SECRET BOX + */ + + @Test + public void secretBoxOpenerIsCleared() { + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(authorizer_valid); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(mockSecretBoxOpener); + + channel.toSubscribeMessage(); + + channel.updateState(ChannelState.UNSUBSCRIBED); + verify(mockSecretBoxOpener).clearKey(); + } +} From ac42fb296a8b38a605737d8af94c5c9bf3a14a17 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Wed, 1 Apr 2020 17:39:20 +0100 Subject: [PATCH 21/48] Remove clear key revisit TODO After reading a bit that seems the best sensible effort feature that can be achieved in Java. --- .../com/pusher/client/crypto/nacl/SecretBoxOpener.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java index ed85944c..85ad6fe4 100644 --- a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java +++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java @@ -26,8 +26,7 @@ a copy of this software and associated documentation files (the "Software"), import static com.pusher.client.util.internal.Preconditions.checkArgument; import static com.pusher.client.util.internal.Preconditions.checkNotNull; - -import java.util.Arrays; +import static java.util.Arrays.fill; public class SecretBoxOpener { @@ -97,13 +96,12 @@ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException { } public void clearKey() { - Arrays.fill(key, (byte) 0); + fill(key, (byte) 0); if (key[0] != 0) { + // so that hopefully the optimiser won't remove the clearing code (best sensible effort) throw new SecurityException("key not cleared correctly"); } key = null; - // TODO: ensure implemented securely (so that the clearing code - // is not removed by compiler's optimisations) } // subKey = byte[32], counter = byte[16], nonce = byte[24], key = byte[32] From 571607c2f95a86c782715e304fe11b99853bc118 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 2 Apr 2020 14:52:32 +0100 Subject: [PATCH 22/48] Add test around clearing the key --- .../com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java index 5b02c3b7..a40cb8b1 100644 --- a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java +++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java @@ -35,4 +35,11 @@ public void openFailsForTamperedCipher() { subject.open(tamperedCipher, nonce); } + + @Test(expected = NullPointerException.class) + public void openFailsAfterClearKey() { + subject.clearKey(); + + subject.open(cipher, nonce); + } } \ No newline at end of file From d003f33dd96a9a206f2b36ca183bb8c1b8cf92fd Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 2 Apr 2020 14:55:25 +0100 Subject: [PATCH 23/48] Mach test data order with param order --- .../java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java index a40cb8b1..46077dc6 100644 --- a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java +++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java @@ -11,8 +11,8 @@ public class SecretBoxOpenerTest { byte[] key = Base64.decode("6071zp2l/GPnDPDXNWTJDHyIZ8pZMvQrYsa4xuTKK2c="); - byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p"); byte[] cipher = Base64.decode("tvttPE2PRQp0bWDmaPyiEU8YJGztmTvTN77OoPwftTNTdDgJXwxHQPE="); + byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p"); SecretBoxOpener subject; From 8ea734027192c5bb8e130b70091572ebaebed4d9 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Thu, 2 Apr 2020 16:16:43 +0100 Subject: [PATCH 24/48] Add clearing of shared secret on disconnected Fix IndexOutOfBounds when passing all 4 args in the example app. Make the example app honour variable length of arguments array. Fix warning. Improve formatting. --- .../impl/PrivateEncryptedChannelImpl.java | 65 ++++++++++++------- .../PrivateEncryptedChannelExampleApp.java | 18 ++--- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 05c46ba5..7cbbfeef 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -6,6 +6,9 @@ import com.pusher.client.channel.PrivateEncryptedChannel; import com.pusher.client.channel.PrivateEncryptedChannelEventListener; import com.pusher.client.channel.SubscriptionEventListener; +import com.pusher.client.connection.ConnectionEventListener; +import com.pusher.client.connection.ConnectionState; +import com.pusher.client.connection.ConnectionStateChange; import com.pusher.client.connection.impl.InternalConnection; import com.pusher.client.crypto.nacl.SecretBoxOpener; import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; @@ -45,22 +48,39 @@ public void bind(final String eventName, final SubscriptionEventListener listene super.bind(eventName, listener); } - private String authenticate() { + @Override + public String toSubscribeMessage() { + String authKey = authenticate(); + // create the data part + final Map dataMap = new LinkedHashMap<>(); + dataMap.put("channel", name); + dataMap.put("auth", authKey); + + // create the wrapper part + final Map jsonObject = new LinkedHashMap<>(); + jsonObject.put("event", "pusher:subscribe"); + jsonObject.put("data", dataMap); + + return GSON.toJson(jsonObject); + } + + private String authenticate() { try { - final Map authResponseMap = GSON.fromJson(getAuthResponse(), Map.class); - final String auth = (String) authResponseMap.get("auth"); - final String sharedSecret = (String) authResponseMap.get("shared_secret"); + @SuppressWarnings("rawtypes") // anything goes in JS + final Map authResponse = GSON.fromJson(getAuthResponse(), Map.class); + + final String auth = (String) authResponse.get("auth"); + final String sharedSecret = (String) authResponse.get("shared_secret"); if (auth == null || sharedSecret == null) { throw new AuthorizationFailureException("Didn't receive all the fields expected " + "from the Authorizer, expected an auth and shared_secret."); } else { - secretBoxOpener = secretBoxOpenerFactory.create( - Base64.decode(sharedSecret)); + secretBoxOpener = secretBoxOpenerFactory.create(Base64.decode(sharedSecret)); + setListenerToClearSecretBoxOpenerOnDisconnected(); return auth; } - } catch (final AuthorizationFailureException e) { throw e; // pass this upwards } catch (final Exception e) { @@ -69,22 +89,22 @@ private String authenticate() { } } - @Override - public String toSubscribeMessage() { - - String authKey = authenticate(); - - // create the data part - final Map dataMap = new LinkedHashMap(); - dataMap.put("channel", name); - dataMap.put("auth", authKey); - - // create the wrapper part - final Map jsonObject = new LinkedHashMap(); - jsonObject.put("event", "pusher:subscribe"); - jsonObject.put("data", dataMap); + private void setListenerToClearSecretBoxOpenerOnDisconnected() { + // For not hanging on to shared secret past the Pusher.disconnect() call, + // i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe + // and hence re-authenticate which creates a new secretBoxOpener. + connection.bind(ConnectionState.DISCONNECTED, new ConnectionEventListener() { + @Override + public void onConnectionStateChange(ConnectionStateChange change) { + tearDownChannel(); // clears & nulls secretBoxOpener + connection.unbind(ConnectionState.DISCONNECTED, this); + } - return GSON.toJson(jsonObject); + @Override + public void onError(String message, String code, Exception e) { + // nop + } + }); } @Override @@ -99,6 +119,7 @@ public void updateState(ChannelState state) { private void tearDownChannel() { if (secretBoxOpener != null) { secretBoxOpener.clearKey(); + secretBoxOpener = null; } } diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 340fdb57..6f19dd05 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -12,7 +12,7 @@ public class PrivateEncryptedChannelExampleApp implements ConnectionEventListener, PrivateEncryptedChannelEventListener { - private String apiKey = "FILL_ME_IN"; + private String apiKey = "FILL_ME_IN"; // "key" at https://dashboard.pusher.com private String channelName = "private-encrypted-channel"; private String eventName = "my-event"; private String cluster = "eu"; @@ -24,12 +24,11 @@ public static void main(final String[] args) { } private PrivateEncryptedChannelExampleApp(final String[] args) { - - if (args.length == 3) { - apiKey = args[0]; - channelName = args[1]; - eventName = args[2]; - cluster = args[3]; + switch (args.length) { + case 4: cluster = args[3]; + case 3: eventName = args[2]; + case 2: channelName = args[1]; + case 0: apiKey = args[0]; } final HttpAuthorizer authorizer = new HttpAuthorizer( @@ -45,7 +44,10 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { // Keep main thread asleep while we watch for events or application will terminate while (true) { try { - Thread.sleep(1000); + Thread.sleep(5000); + pusher.disconnect(); // to put clearing of shared secret on disconnected to test + Thread.sleep(5000); + pusher.connect(this); } catch (final InterruptedException e) { e.printStackTrace(); From ddda077437ecf46a1f1afff6a37d144beeefc690 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 11:47:46 +0100 Subject: [PATCH 25/48] Make unsubscribe to remove disconnect listener Make the code more legible. That is more symmetrical when it comes to setting/removing the disconnect listener, and creating/disposing SecretBoxOpener. Make the example app exercise unsubscribe too and the two combinations (subscribed/unsubscribed) when disconnect is called. --- .../impl/PrivateEncryptedChannelImpl.java | 48 +++++++++++-------- .../PrivateEncryptedChannelExampleApp.java | 12 +++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 7cbbfeef..8451ba50 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -25,6 +25,21 @@ public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateE private SecretBoxOpenerFactory secretBoxOpenerFactory; private SecretBoxOpener secretBoxOpener; + // For not hanging on to shared secret past the Pusher.disconnect() call, + // i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe + // and hence re-authenticate which creates a new secretBoxOpener. + private ConnectionEventListener onDisconnectedListener = new ConnectionEventListener() { + @Override + public void onConnectionStateChange(ConnectionStateChange change) { + disposeSecretBoxOpener(); + } + + @Override + public void onError(String message, String code, Exception e) { + // nop + } + }; + public PrivateEncryptedChannelImpl(final InternalConnection connection, final String channelName, final Authorizer authorizer, @@ -77,8 +92,7 @@ private String authenticate() { throw new AuthorizationFailureException("Didn't receive all the fields expected " + "from the Authorizer, expected an auth and shared_secret."); } else { - secretBoxOpener = secretBoxOpenerFactory.create(Base64.decode(sharedSecret)); - setListenerToClearSecretBoxOpenerOnDisconnected(); + createSecretBoxOpener(Base64.decode(sharedSecret)); return auth; } } catch (final AuthorizationFailureException e) { @@ -89,22 +103,13 @@ private String authenticate() { } } - private void setListenerToClearSecretBoxOpenerOnDisconnected() { - // For not hanging on to shared secret past the Pusher.disconnect() call, - // i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe - // and hence re-authenticate which creates a new secretBoxOpener. - connection.bind(ConnectionState.DISCONNECTED, new ConnectionEventListener() { - @Override - public void onConnectionStateChange(ConnectionStateChange change) { - tearDownChannel(); // clears & nulls secretBoxOpener - connection.unbind(ConnectionState.DISCONNECTED, this); - } + private void createSecretBoxOpener(byte[] key) { + secretBoxOpener = secretBoxOpenerFactory.create(key); + setListenerToClearSecretBoxOpenerOnDisconnected(); + } - @Override - public void onError(String message, String code, Exception e) { - // nop - } - }); + private void setListenerToClearSecretBoxOpenerOnDisconnected() { + connection.bind(ConnectionState.DISCONNECTED, onDisconnectedListener); } @Override @@ -112,17 +117,22 @@ public void updateState(ChannelState state) { super.updateState(state); if (state == ChannelState.UNSUBSCRIBED) { - tearDownChannel(); + disposeSecretBoxOpener(); } } - private void tearDownChannel() { + private void disposeSecretBoxOpener() { if (secretBoxOpener != null) { secretBoxOpener.clearKey(); secretBoxOpener = null; + removeListenerToClearSecretBoxOpenerOnDisconnected(); } } + private void removeListenerToClearSecretBoxOpenerOnDisconnected() { + connection.unbind(ConnectionState.DISCONNECTED, onDisconnectedListener); + } + private String getAuthResponse() { final String socketId = connection.getSocketId(); return authorizer.authorize(getName(), socketId); diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 6f19dd05..910ee70d 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -17,7 +17,7 @@ public class PrivateEncryptedChannelExampleApp implements private String eventName = "my-event"; private String cluster = "eu"; - private final PrivateEncryptedChannel channel; + private PrivateEncryptedChannel channel; public static void main(final String[] args) { new PrivateEncryptedChannelExampleApp(args); @@ -42,12 +42,16 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); // Keep main thread asleep while we watch for events or application will terminate - while (true) { + for (int i = 0; ; i++) { try { Thread.sleep(5000); - pusher.disconnect(); // to put clearing of shared secret on disconnected to test - Thread.sleep(5000); + pusher.disconnect(); // to test clearing of shared secret pusher.connect(this); + Thread.sleep(5000); + pusher.unsubscribe(channelName); // to test clearing of shared secret + if (i % 2 == 0) { // to test disconnect on both unsubscribed/subscribed + channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); + } } catch (final InterruptedException e) { e.printStackTrace(); From 8ae2cb2ee6cda4208d429f5ff7ef632a7b0940ad Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 11:54:11 +0100 Subject: [PATCH 26/48] Add info about the need for tmp log for the semi-manual test --- .../client/example/PrivateEncryptedChannelExampleApp.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 910ee70d..45331401 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -45,10 +45,10 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { for (int i = 0; ; i++) { try { Thread.sleep(5000); - pusher.disconnect(); // to test clearing of shared secret + pusher.disconnect(); // to test clearing of shared secret (via tmp log) pusher.connect(this); Thread.sleep(5000); - pusher.unsubscribe(channelName); // to test clearing of shared secret + pusher.unsubscribe(channelName); // to test clearing of shared secret (via tmp log) if (i % 2 == 0) { // to test disconnect on both unsubscribed/subscribed channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); } From 670609e704b672dac79f91b2667b7c5cd10ce9b0 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 12:00:41 +0100 Subject: [PATCH 27/48] Fix IndexOutOfBounds for no args in example app Fix accepting a single arg. Thanks Mike for catching it: https://github.com/pusher/pusher-websocket-java/pull/247#discussion_r402922558 --- .../client/example/PrivateEncryptedChannelExampleApp.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 45331401..589569ad 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -28,7 +28,7 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { case 4: cluster = args[3]; case 3: eventName = args[2]; case 2: channelName = args[1]; - case 0: apiKey = args[0]; + case 1: apiKey = args[0]; } final HttpAuthorizer authorizer = new HttpAuthorizer( From b5059d3b579da864abc23c0cd8044fe47f9dbe23 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 14:16:02 +0100 Subject: [PATCH 28/48] Make naming consistent and more clear --- .../impl/PrivateEncryptedChannelImpl.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 8451ba50..2d54cd85 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -28,7 +28,9 @@ public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateE // For not hanging on to shared secret past the Pusher.disconnect() call, // i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe // and hence re-authenticate which creates a new secretBoxOpener. - private ConnectionEventListener onDisconnectedListener = new ConnectionEventListener() { + private ConnectionEventListener disposeSecretBoxOpenerOnDisconnectedListener = + new ConnectionEventListener() { + @Override public void onConnectionStateChange(ConnectionStateChange change) { disposeSecretBoxOpener(); @@ -105,11 +107,12 @@ private String authenticate() { private void createSecretBoxOpener(byte[] key) { secretBoxOpener = secretBoxOpenerFactory.create(key); - setListenerToClearSecretBoxOpenerOnDisconnected(); + setListenerToDisposeSecretBoxOpenerOnDisconnected(); } - private void setListenerToClearSecretBoxOpenerOnDisconnected() { - connection.bind(ConnectionState.DISCONNECTED, onDisconnectedListener); + private void setListenerToDisposeSecretBoxOpenerOnDisconnected() { + connection.bind(ConnectionState.DISCONNECTED, + disposeSecretBoxOpenerOnDisconnectedListener); } @Override @@ -125,12 +128,13 @@ private void disposeSecretBoxOpener() { if (secretBoxOpener != null) { secretBoxOpener.clearKey(); secretBoxOpener = null; - removeListenerToClearSecretBoxOpenerOnDisconnected(); + removeListenerToDisposeSecretBoxOpenerOnDisconnected(); } } - private void removeListenerToClearSecretBoxOpenerOnDisconnected() { - connection.unbind(ConnectionState.DISCONNECTED, onDisconnectedListener); + private void removeListenerToDisposeSecretBoxOpenerOnDisconnected() { + connection.unbind(ConnectionState.DISCONNECTED, + disposeSecretBoxOpenerOnDisconnectedListener); } private String getAuthResponse() { From 9c8bf51a9d48cb03b586afd535fffe2f7116ebc1 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 17:31:42 +0100 Subject: [PATCH 29/48] Add PrivateEncryptedChannelClearsKeyTest Consistently tidy up PrivateEncryptedChannelImplTest --- .../PrivateEncryptedChannelClearsKeyTest.java | 97 +++++++++++++++++++ .../impl/PrivateEncryptedChannelImplTest.java | 86 +++++----------- 2 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java new file mode 100644 index 00000000..0f936531 --- /dev/null +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java @@ -0,0 +1,97 @@ +package com.pusher.client.channel.impl; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import com.pusher.client.Authorizer; +import com.pusher.client.channel.ChannelState; +import com.pusher.client.connection.ConnectionEventListener; +import com.pusher.client.connection.ConnectionState; +import com.pusher.client.connection.ConnectionStateChange; +import com.pusher.client.connection.impl.InternalConnection; +import com.pusher.client.crypto.nacl.SecretBoxOpener; +import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; +import com.pusher.client.util.Factory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class PrivateEncryptedChannelClearsKeyTest { + + final String CHANNEL_NAME = "private-encrypted-unit-test-channel"; + final String AUTH_RESPONSE = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; + + @Mock + InternalConnection mockInternalConnection; + @Mock + Authorizer mockAuthorizer; + @Mock + Factory mockFactory; + + @Mock + SecretBoxOpenerFactory mockSecretBoxOpenerFactory; + @Mock + SecretBoxOpener mockSecretBoxOpener; + + @Captor + ArgumentCaptor connectionEventListenerArgumentCaptor; + + PrivateEncryptedChannelImpl subject; + + @Before + public void setUp() { + when(mockAuthorizer.authorize(eq(CHANNEL_NAME), anyString())).thenReturn(AUTH_RESPONSE); + when(mockSecretBoxOpenerFactory.create(any())).thenReturn(mockSecretBoxOpener); + + subject = new PrivateEncryptedChannelImpl(mockInternalConnection, CHANNEL_NAME, + mockAuthorizer, mockFactory, mockSecretBoxOpenerFactory); + } + + @Test + public void secretBoxOpenerIsClearedOnUnsubscribed() { + subject.toSubscribeMessage(); + + subject.updateState(ChannelState.UNSUBSCRIBED); + + verify(mockSecretBoxOpener).clearKey(); + } + + @Test + public void secretBoxOpenerIsClearedOnDisconnected() { + doAnswer((Answer) invocation -> { + ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1]; + l.onConnectionStateChange(new ConnectionStateChange( + ConnectionState.DISCONNECTING, + ConnectionState.DISCONNECTED + )); + return null; + }).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any()); + subject.toSubscribeMessage(); + + verify(mockSecretBoxOpener).clearKey(); + } + + @Test + public void secretBoxOpenerIsClearedOnceOnUnsubscribedAndThenDisconnected() { + doAnswer((Answer) invocation -> { + subject.updateState(ChannelState.UNSUBSCRIBED); + + ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1]; + l.onConnectionStateChange(new ConnectionStateChange( + ConnectionState.DISCONNECTING, + ConnectionState.DISCONNECTED + )); + + return null; + }).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any()); + subject.toSubscribeMessage(); + + verify(mockSecretBoxOpener).clearKey(); + } +} diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java index 025872b5..d9e639bb 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -1,15 +1,17 @@ package com.pusher.client.channel.impl; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.pusher.client.AuthorizationFailureException; import com.pusher.client.Authorizer; import com.pusher.client.channel.ChannelEventListener; -import com.pusher.client.channel.ChannelState; import com.pusher.client.channel.PrivateEncryptedChannelEventListener; import com.pusher.client.connection.impl.InternalConnection; -import com.pusher.client.crypto.nacl.SecretBoxOpener; import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; -import com.pusher.client.util.Factory; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -17,42 +19,31 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(MockitoJUnitRunner.class) public class PrivateEncryptedChannelImplTest extends ChannelImplTest { - String authorizer_valid = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; - String authorizer_missingAuthKey = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; - String authorizer_missingSharedSecret = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; - String authorizer_malformedJson = "potatoes"; - + final String AUTH_RESPONSE = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; + final String AUTH_RESPONSE_MISSING_AUTH = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; + final String AUTH_RESPONSE_MISSING_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; + final String AUTH_RESPONSE_INVALID_JSON = "potatoes"; @Mock InternalConnection mockInternalConnection; @Mock Authorizer mockAuthorizer; @Mock - Factory mockFactory; - @Mock SecretBoxOpenerFactory mockSecretBoxOpenerFactory; - @Mock - SecretBoxOpener mockSecretBoxOpener; - @Override @Before public void setUp() { super.setUp(); - when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(authorizer_valid); + when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(AUTH_RESPONSE); + } + + protected PrivateEncryptedChannelImpl newInstance() { + return new PrivateEncryptedChannelImpl(mockInternalConnection, getChannelName(), + mockAuthorizer, factory, mockSecretBoxOpenerFactory); } @Override @@ -117,11 +108,9 @@ public void testReturnsCorrectSubscribeMessage() { @Test public void authenticationSucceedsGivenValidAuthorizer() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_valid); + .thenReturn(AUTH_RESPONSE); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -133,11 +122,9 @@ protected ChannelEventListener getEventListener() { @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfNoAuthKey() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_missingAuthKey); + .thenReturn(AUTH_RESPONSE_MISSING_AUTH); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -145,11 +132,9 @@ mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfNoSharedSecret() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_missingSharedSecret); + .thenReturn(AUTH_RESPONSE_MISSING_SHARED_SECRET); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -157,33 +142,10 @@ mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfMalformedJson() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_malformedJson); + .thenReturn(AUTH_RESPONSE_INVALID_JSON); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } - - /* - TESTING SECRET BOX - */ - - @Test - public void secretBoxOpenerIsCleared() { - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); - - when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_valid); - when(mockSecretBoxOpenerFactory.create(any())) - .thenReturn(mockSecretBoxOpener); - - channel.toSubscribeMessage(); - - channel.updateState(ChannelState.UNSUBSCRIBED); - verify(mockSecretBoxOpener).clearKey(); - } } From 95d5ca30c758f62defb5dc7ca7c687c54cdbfc31 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 17:34:37 +0100 Subject: [PATCH 30/48] Revert making example app unsubscribe/disconnect Following Danielle's feedback: https://github.com/pusher/pusher-websocket-java/pull/247/files#r402989472 --- .../example/PrivateEncryptedChannelExampleApp.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 589569ad..b883e60f 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -42,16 +42,9 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); // Keep main thread asleep while we watch for events or application will terminate - for (int i = 0; ; i++) { + while (true) { try { - Thread.sleep(5000); - pusher.disconnect(); // to test clearing of shared secret (via tmp log) - pusher.connect(this); - Thread.sleep(5000); - pusher.unsubscribe(channelName); // to test clearing of shared secret (via tmp log) - if (i % 2 == 0) { // to test disconnect on both unsubscribed/subscribed - channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); - } + Thread.sleep(1000); } catch (final InterruptedException e) { e.printStackTrace(); From 0250472be8bb254b0edf286b47fbd2b086be05c7 Mon Sep 17 00:00:00 2001 From: Marek Urbaniak <8502071+marekoid@users.noreply.github.com> Date: Fri, 3 Apr 2020 17:39:31 +0100 Subject: [PATCH 31/48] Remove unused ArgumentCaptor --- .../channel/impl/PrivateEncryptedChannelClearsKeyTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java index 0f936531..19521a7d 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java @@ -15,8 +15,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; @@ -39,9 +37,6 @@ public class PrivateEncryptedChannelClearsKeyTest { @Mock SecretBoxOpener mockSecretBoxOpener; - @Captor - ArgumentCaptor connectionEventListenerArgumentCaptor; - PrivateEncryptedChannelImpl subject; @Before From 3a8a7bc2114eeaadb06faf2426633ba578334a3b Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 10:28:18 +0100 Subject: [PATCH 32/48] Add. prepareEvent method to InternalChannel interface and implement in ChannelImpl --- .../client/channel/impl/ChannelImpl.java | 23 ++++++++++++------- .../client/channel/impl/InternalChannel.java | 3 +++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java index 4ae849b6..97d44a95 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java @@ -89,6 +89,11 @@ public boolean isSubscribed() { /* InternalChannel implementation */ + @Override + public PusherEvent prepareEvent(String event, String message) { + return GSON.fromJson(message, PusherEvent.class); + } + @Override public void onMessage(final String event, final String message) { @@ -108,14 +113,16 @@ public void onMessage(final String event, final String message) { } if (listeners != null) { - for (final SubscriptionEventListener listener : listeners) { - final PusherEvent e = GSON.fromJson(message, PusherEvent.class); - factory.queueOnEventThread(new Runnable() { - @Override - public void run() { - listener.onEvent(e); - } - }); + final PusherEvent pusherEvent = prepareEvent(event, message); + if (pusherEvent != null) { + for (final SubscriptionEventListener listener : listeners) { + factory.queueOnEventThread(new Runnable() { + @Override + public void run() { + listener.onEvent(pusherEvent); + } + }); + } } } } diff --git a/src/main/java/com/pusher/client/channel/impl/InternalChannel.java b/src/main/java/com/pusher/client/channel/impl/InternalChannel.java index e1668b7a..b6d8e032 100644 --- a/src/main/java/com/pusher/client/channel/impl/InternalChannel.java +++ b/src/main/java/com/pusher/client/channel/impl/InternalChannel.java @@ -3,6 +3,7 @@ import com.pusher.client.channel.Channel; import com.pusher.client.channel.ChannelEventListener; import com.pusher.client.channel.ChannelState; +import com.pusher.client.channel.PusherEvent; public interface InternalChannel extends Channel, Comparable { @@ -10,6 +11,8 @@ public interface InternalChannel extends Channel, Comparable { String toUnsubscribeMessage(); + PusherEvent prepareEvent(String event, String message); + void onMessage(String event, String message); void updateState(ChannelState state); From c8ea300047500d57dc8823088c92fa02ecc4b3df Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 10:45:33 +0100 Subject: [PATCH 33/48] Refactor getInterestedListeners to it's own method in ChannelImpl --- .../client/channel/impl/ChannelImpl.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java index 97d44a95..8bb49e46 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java @@ -99,19 +99,8 @@ public void onMessage(final String event, final String message) { if (event.equals(SUBSCRIPTION_SUCCESS_EVENT)) { updateState(ChannelState.SUBSCRIBED); - } - else { - final Set listeners; - synchronized (lock) { - final Set sharedListeners = eventNameToListenerMap.get(event); - if (sharedListeners != null) { - listeners = new HashSet(sharedListeners); - } - else { - listeners = null; - } - } - + } else { + final Set listeners = getInterestedListeners(event); if (listeners != null) { final PusherEvent pusherEvent = prepareEvent(event, message); if (pusherEvent != null) { @@ -220,4 +209,17 @@ private void validateArguments(final String eventName, final SubscriptionEventLi "Cannot bind or unbind to events on a channel that has been unsubscribed. Call Pusher.subscribe() to resubscribe to this channel"); } } + + protected Set getInterestedListeners(String event) { + final Set listeners; + synchronized (lock) { + final Set sharedListeners = eventNameToListenerMap.get(event); + if (sharedListeners != null) { + listeners = new HashSet(sharedListeners); + } else { + listeners = null; + } + } + return listeners; + } } From f290cb6f60667a05678329b72a6c8cb0717342b7 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 10:48:09 +0100 Subject: [PATCH 34/48] Decrypt PrivateEncrypted messages and retry once --- .../PrivateEncryptedChannelEventListener.java | 2 +- .../impl/PrivateEncryptedChannelImpl.java | 70 ++++++++++++++++++ .../PrivateEncryptedChannelExampleApp.java | 6 ++ .../impl/PrivateEncryptedChannelImplTest.java | 74 +++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java index a6aeeaae..3994dc83 100644 --- a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java +++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java @@ -6,5 +6,5 @@ */ public interface PrivateEncryptedChannelEventListener extends PrivateChannelEventListener { - // TODO: add onDecryptionFailure + void onDecryptionFailure(Exception e); } diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 2d54cd85..5d5d75ce 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -5,18 +5,22 @@ import com.pusher.client.channel.ChannelState; import com.pusher.client.channel.PrivateEncryptedChannel; import com.pusher.client.channel.PrivateEncryptedChannelEventListener; +import com.pusher.client.channel.PusherEvent; import com.pusher.client.channel.SubscriptionEventListener; import com.pusher.client.connection.ConnectionEventListener; import com.pusher.client.connection.ConnectionState; import com.pusher.client.connection.ConnectionStateChange; import com.pusher.client.connection.impl.InternalConnection; +import com.pusher.client.crypto.nacl.AuthenticityException; import com.pusher.client.crypto.nacl.SecretBoxOpener; import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; import com.pusher.client.util.Factory; import com.pusher.client.util.internal.Base64; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateEncryptedChannel { @@ -124,6 +128,72 @@ public void updateState(ChannelState state) { } } + @Override + public PusherEvent prepareEvent(String event, String message) { + try { + + Map receivedMessage = GSON.fromJson(message, Map.class); + final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); + receivedMessage.replace("data", decryptedMessage); + + return GSON.fromJson( + GSON.toJson(receivedMessage), PusherEvent.class); + + } catch (AuthenticityException e1) { + + // retry once only. + disposeSecretBoxOpener(); + authenticate(); + + try { + Map receivedMessage = GSON.fromJson(message, Map.class); + final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); + receivedMessage.replace("data", decryptedMessage); + + return GSON.fromJson( + GSON.toJson(receivedMessage), PusherEvent.class); + } catch (AuthenticityException e2) { + disposeSecretBoxOpener(); + notifyListenersOfDecryptFailure(event); + } + } + + return null; + } + + private void notifyListenersOfDecryptFailure(final String event) { + Set listeners = getInterestedListeners(event); + if (listeners != null) { + for (SubscriptionEventListener listener : listeners) { + ((PrivateEncryptedChannelEventListener)listener).onDecryptionFailure( + new Exception("Failed to decrypt message")); + } + } + } + + private class EncryptedReceivedData { + String nonce; + String ciphertext; + + public byte[] getNonce() { + return Base64.decode(nonce); + } + + public byte[] getCiphertext() { + return Base64.decode(ciphertext); + } + } + + private String decryptMessage(String data) { + + final EncryptedReceivedData encryptedReceivedData = + GSON.fromJson(data, EncryptedReceivedData.class); + + return new String(secretBoxOpener.open( + encryptedReceivedData.getCiphertext(), + encryptedReceivedData.getNonce())); + } + private void disposeSecretBoxOpener() { if (secretBoxOpener != null) { secretBoxOpener.clearKey(); diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index b883e60f..24d2feb3 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -86,4 +86,10 @@ public void onError(String message, String code, Exception e) { code, e)); } + + @Override + public void onDecryptionFailure(Exception e) { + System.out.println(String.format( + "An error was received decrypting message - exception: [%s]", e)); + } } diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java index d9e639bb..88e1d224 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -1,9 +1,12 @@ package com.pusher.client.channel.impl; import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.pusher.client.AuthorizationFailureException; @@ -11,7 +14,10 @@ import com.pusher.client.channel.ChannelEventListener; import com.pusher.client.channel.PrivateEncryptedChannelEventListener; import com.pusher.client.connection.impl.InternalConnection; +import com.pusher.client.crypto.nacl.SecretBoxOpener; import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; +import com.pusher.client.util.internal.Base64; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,6 +32,7 @@ public class PrivateEncryptedChannelImplTest extends ChannelImplTest { final String AUTH_RESPONSE_MISSING_AUTH = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; final String AUTH_RESPONSE_MISSING_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; final String AUTH_RESPONSE_INVALID_JSON = "potatoes"; + final String SHARED_SECRET = "iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo="; @Mock InternalConnection mockInternalConnection; @@ -148,4 +155,71 @@ public void authenticationThrowsExceptionIfMalformedJson() { channel.toSubscribeMessage(); } + + /* + ON MESSAGE + */ + @Test + public void testDataIsExtractedFromMessageAndPassedToSingleListener() { + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener = mock(PrivateEncryptedChannelEventListener.class); + + channel.bind("my-event", mockListener); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener, times(1)).onEvent(argCaptor.capture()); + assertEquals("event1", argCaptor.getValue().getEventName()); + assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); + } + + @Test + public void testDataIsExtractedFromMessageAndPassedToMultipleListeners() { + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class); + PrivateEncryptedChannelEventListener mockListener2 = mock(PrivateEncryptedChannelEventListener.class); + + channel.bind("my-event", mockListener1); + channel.bind("my-event", mockListener2); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1).onEvent(argCaptor.capture()); + assertEquals("event1", argCaptor.getValue().getEventName()); + assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); + + verify(mockListener2).onEvent(argCaptor.capture()); + assertEquals("event1", argCaptor.getValue().getEventName()); + assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); + } } From 0cf29b77af9f2192966cc3dc5e32fec56a93fccc Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 14:10:12 +0100 Subject: [PATCH 35/48] Add tests for retrying decrypting messages --- .../impl/PrivateEncryptedChannelImpl.java | 1 - .../impl/PrivateEncryptedChannelImplTest.java | 66 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 5d5d75ce..ba60cb93 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -17,7 +17,6 @@ import com.pusher.client.util.Factory; import com.pusher.client.util.internal.Base64; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java index 88e1d224..81b7590f 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -21,6 +21,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -33,6 +35,8 @@ public class PrivateEncryptedChannelImplTest extends ChannelImplTest { final String AUTH_RESPONSE_MISSING_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; final String AUTH_RESPONSE_INVALID_JSON = "potatoes"; final String SHARED_SECRET = "iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo="; + final String AUTH_RESPONSE_INCORRECT_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5do0=\"}"; + final String SHARED_SECRET_INCORRECT = "iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5do0="; @Mock InternalConnection mockInternalConnection; @@ -41,6 +45,9 @@ public class PrivateEncryptedChannelImplTest extends ChannelImplTest { @Mock SecretBoxOpenerFactory mockSecretBoxOpenerFactory; + @Captor + ArgumentCaptor exceptionArgumentCaptor; + @Override @Before public void setUp() { @@ -222,4 +229,63 @@ public void testDataIsExtractedFromMessageAndPassedToMultipleListeners() { assertEquals("event1", argCaptor.getValue().getEventName()); assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); } + + @Test + public void onMessageRaisesExceptionWhenFailingToDecryptTwice() { + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class); + channel.bind("my-event", mockListener1); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1).onDecryptionFailure(exceptionArgumentCaptor.capture()); + } + + @Test + public void onMessageRetriesDecryptionOnce() { + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class); + channel.bind("my-event", mockListener1); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1).onEvent(argCaptor.capture()); + assertEquals("event1", argCaptor.getValue().getEventName()); + assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); + } + } From 0e178e8ab08fcad4c3f340a6b4f431b889991b84 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:09:26 +0100 Subject: [PATCH 36/48] Simplify the getInterestedListeners method in ChannelImpl --- .../java/com/pusher/client/channel/impl/ChannelImpl.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java index 8bb49e46..157fc1c2 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java @@ -211,15 +211,8 @@ private void validateArguments(final String eventName, final SubscriptionEventLi } protected Set getInterestedListeners(String event) { - final Set listeners; synchronized (lock) { - final Set sharedListeners = eventNameToListenerMap.get(event); - if (sharedListeners != null) { - listeners = new HashSet(sharedListeners); - } else { - listeners = null; - } + return eventNameToListenerMap.get(event); } - return listeners; } } From 4bfc70de174c9abae8c6a012e29a2507c3570fd9 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:29:59 +0100 Subject: [PATCH 37/48] Handle multiple failed decryption calls better --- .../PrivateEncryptedChannelEventListener.java | 2 +- .../impl/PrivateEncryptedChannelImpl.java | 42 ++++++------ .../PrivateEncryptedChannelExampleApp.java | 4 +- .../impl/PrivateEncryptedChannelImplTest.java | 64 ++++++++++++++----- 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java index 3994dc83..119fc58a 100644 --- a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java +++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java @@ -6,5 +6,5 @@ */ public interface PrivateEncryptedChannelEventListener extends PrivateChannelEventListener { - void onDecryptionFailure(Exception e); + void onDecryptionFailure(String event, String reason); } diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index ba60cb93..e9844dfd 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -129,43 +129,49 @@ public void updateState(ChannelState state) { @Override public PusherEvent prepareEvent(String event, String message) { - try { - - Map receivedMessage = GSON.fromJson(message, Map.class); - final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); - receivedMessage.replace("data", decryptedMessage); - - return GSON.fromJson( - GSON.toJson(receivedMessage), PusherEvent.class); - } catch (AuthenticityException e1) { - - // retry once only. - disposeSecretBoxOpener(); - authenticate(); + if (secretBoxOpener == null) { + notifyListenersOfDecryptFailure(event, "Too many failed attempts to decrypt a previous message."); + } else { try { + Map receivedMessage = GSON.fromJson(message, Map.class); final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); receivedMessage.replace("data", decryptedMessage); return GSON.fromJson( GSON.toJson(receivedMessage), PusherEvent.class); - } catch (AuthenticityException e2) { + + } catch (AuthenticityException e1) { + + // retry once only. disposeSecretBoxOpener(); - notifyListenersOfDecryptFailure(event); + authenticate(); + + try { + Map receivedMessage = GSON.fromJson(message, Map.class); + final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); + receivedMessage.replace("data", decryptedMessage); + + return GSON.fromJson( + GSON.toJson(receivedMessage), PusherEvent.class); + } catch (AuthenticityException e2) { + disposeSecretBoxOpener(); + notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); + } } + } - return null; } - private void notifyListenersOfDecryptFailure(final String event) { + private void notifyListenersOfDecryptFailure(final String event, final String reason) { Set listeners = getInterestedListeners(event); if (listeners != null) { for (SubscriptionEventListener listener : listeners) { ((PrivateEncryptedChannelEventListener)listener).onDecryptionFailure( - new Exception("Failed to decrypt message")); + event, reason); } } } diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 24d2feb3..28f4a6ff 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -88,8 +88,8 @@ public void onError(String message, String code, Exception e) { } @Override - public void onDecryptionFailure(Exception e) { + public void onDecryptionFailure(String event, String reason) { System.out.println(String.format( - "An error was received decrypting message - exception: [%s]", e)); + "An error was received decrypting message for event:[%s] - reason: [%s]", event, reason)); } } diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java index 81b7590f..5b781259 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -1,14 +1,5 @@ package com.pusher.client.channel.impl; -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.pusher.client.AuthorizationFailureException; import com.pusher.client.Authorizer; import com.pusher.client.channel.ChannelEventListener; @@ -21,12 +12,19 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @RunWith(MockitoJUnitRunner.class) public class PrivateEncryptedChannelImplTest extends ChannelImplTest { @@ -45,9 +43,6 @@ public class PrivateEncryptedChannelImplTest extends ChannelImplTest { @Mock SecretBoxOpenerFactory mockSecretBoxOpenerFactory; - @Captor - ArgumentCaptor exceptionArgumentCaptor; - @Override @Before public void setUp() { @@ -255,7 +250,7 @@ public void onMessageRaisesExceptionWhenFailingToDecryptTwice() { "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + "}\"}"); - verify(mockListener1).onDecryptionFailure(exceptionArgumentCaptor.capture()); + verify(mockListener1).onDecryptionFailure(anyString(), anyString()); } @Test @@ -288,4 +283,43 @@ public void onMessageRetriesDecryptionOnce() { assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); } + @Test + public void twoEventsReceivedWithIncorrectSharedSecret() { + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class); + channel.bind("my-event", mockListener1); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1).onDecryptionFailure("my-event", "Failed to decrypt message."); + + // send a second message + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1).onDecryptionFailure("my-event", "Too many failed attempts to decrypt a previous message."); + } + } From 3a8f63ecf0527ad80fc50dbcda7ddbb76a1510c5 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:40:34 +0100 Subject: [PATCH 38/48] When decrypting a message, pass the json map to the PusherEvent constructor --- .../channel/impl/PrivateEncryptedChannelImpl.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index e9844dfd..269442f4 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -136,12 +136,12 @@ public PusherEvent prepareEvent(String event, String message) { try { - Map receivedMessage = GSON.fromJson(message, Map.class); + Map receivedMessage = + GSON.>fromJson(message, Map.class); final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); receivedMessage.replace("data", decryptedMessage); - return GSON.fromJson( - GSON.toJson(receivedMessage), PusherEvent.class); + return new PusherEvent(receivedMessage); } catch (AuthenticityException e1) { @@ -150,12 +150,12 @@ public PusherEvent prepareEvent(String event, String message) { authenticate(); try { - Map receivedMessage = GSON.fromJson(message, Map.class); + Map receivedMessage = + GSON.>fromJson(message, Map.class); final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); receivedMessage.replace("data", decryptedMessage); - return GSON.fromJson( - GSON.toJson(receivedMessage), PusherEvent.class); + return new PusherEvent(receivedMessage); } catch (AuthenticityException e2) { disposeSecretBoxOpener(); notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); From b03e46690292a35b8b81066280e4257f2348d6fc Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:47:40 +0100 Subject: [PATCH 39/48] Refactor decryptMessage to package up as a PusherEvent --- .../impl/PrivateEncryptedChannelImpl.java | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 269442f4..c50a1d3b 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -135,14 +135,7 @@ public PusherEvent prepareEvent(String event, String message) { } else { try { - - Map receivedMessage = - GSON.>fromJson(message, Map.class); - final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); - receivedMessage.replace("data", decryptedMessage); - - return new PusherEvent(receivedMessage); - + return decryptMessage(message); } catch (AuthenticityException e1) { // retry once only. @@ -150,12 +143,7 @@ public PusherEvent prepareEvent(String event, String message) { authenticate(); try { - Map receivedMessage = - GSON.>fromJson(message, Map.class); - final String decryptedMessage = decryptMessage((String) receivedMessage.get("data")); - receivedMessage.replace("data", decryptedMessage); - - return new PusherEvent(receivedMessage); + return decryptMessage(message); } catch (AuthenticityException e2) { disposeSecretBoxOpener(); notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); @@ -189,14 +177,21 @@ public byte[] getCiphertext() { } } - private String decryptMessage(String data) { + private PusherEvent decryptMessage(String message) { + + Map receivedMessage = + GSON.>fromJson(message, Map.class); final EncryptedReceivedData encryptedReceivedData = - GSON.fromJson(data, EncryptedReceivedData.class); + GSON.fromJson((String)receivedMessage.get("data"), EncryptedReceivedData.class); - return new String(secretBoxOpener.open( + String decryptedData = new String(secretBoxOpener.open( encryptedReceivedData.getCiphertext(), encryptedReceivedData.getNonce())); + + receivedMessage.replace("data", decryptedData); + + return new PusherEvent(receivedMessage); } private void disposeSecretBoxOpener() { From a87547f6940ff3f6482bcba7956ebfd476470e1d Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:51:14 +0100 Subject: [PATCH 40/48] Move the retry logic into it's own method --- .../impl/PrivateEncryptedChannelImpl.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index c50a1d3b..f5d5f845 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -138,22 +138,27 @@ public PusherEvent prepareEvent(String event, String message) { return decryptMessage(message); } catch (AuthenticityException e1) { - // retry once only. - disposeSecretBoxOpener(); - authenticate(); - - try { - return decryptMessage(message); - } catch (AuthenticityException e2) { - disposeSecretBoxOpener(); - notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); - } + retryDecryptingMessage(event, message); } } return null; } + private PusherEvent retryDecryptingMessage(String event, String message) { + + disposeSecretBoxOpener(); + authenticate(); + + try { + return decryptMessage(message); + } catch (AuthenticityException e2) { + disposeSecretBoxOpener(); + notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); + } + return null; + } + private void notifyListenersOfDecryptFailure(final String event, final String reason) { Set listeners = getInterestedListeners(event); if (listeners != null) { From 2f5b7e27aae3eec3182faa9678d00f6ae2532747 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Mon, 6 Apr 2020 16:54:25 +0100 Subject: [PATCH 41/48] Revert "Move the retry logic into it's own method" This reverts commit a87547f6940ff3f6482bcba7956ebfd476470e1d. --- .../impl/PrivateEncryptedChannelImpl.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index f5d5f845..c50a1d3b 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -138,27 +138,22 @@ public PusherEvent prepareEvent(String event, String message) { return decryptMessage(message); } catch (AuthenticityException e1) { - retryDecryptingMessage(event, message); + // retry once only. + disposeSecretBoxOpener(); + authenticate(); + + try { + return decryptMessage(message); + } catch (AuthenticityException e2) { + disposeSecretBoxOpener(); + notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); + } } } return null; } - private PusherEvent retryDecryptingMessage(String event, String message) { - - disposeSecretBoxOpener(); - authenticate(); - - try { - return decryptMessage(message); - } catch (AuthenticityException e2) { - disposeSecretBoxOpener(); - notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); - } - return null; - } - private void notifyListenersOfDecryptFailure(final String event, final String reason) { Set listeners = getInterestedListeners(event); if (listeners != null) { From 0a62b935af356bed5f263264257eca86ce5b8950 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Tue, 7 Apr 2020 09:59:02 +0100 Subject: [PATCH 42/48] Keep the shared_secret after the second retry so any subsequent messages get a retry to the authorization endpoint --- .../impl/PrivateEncryptedChannelImpl.java | 28 +++++------ .../impl/PrivateEncryptedChannelImplTest.java | 47 +++++++++++++++++-- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index c50a1d3b..66a9c329 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -130,27 +130,23 @@ public void updateState(ChannelState state) { @Override public PusherEvent prepareEvent(String event, String message) { - if (secretBoxOpener == null) { - notifyListenersOfDecryptFailure(event, "Too many failed attempts to decrypt a previous message."); - } else { + try { + return decryptMessage(message); + } catch (AuthenticityException e1) { + + // retry once only. + disposeSecretBoxOpener(); + authenticate(); try { return decryptMessage(message); - } catch (AuthenticityException e1) { - - // retry once only. - disposeSecretBoxOpener(); - authenticate(); - - try { - return decryptMessage(message); - } catch (AuthenticityException e2) { - disposeSecretBoxOpener(); - notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); - } + } catch (AuthenticityException e2) { + // deliberately not destroying the secretBoxOpener so the next message + // has an opportunity to fetch a new key and decrypt + notifyListenersOfDecryptFailure(event, "Failed to decrypt message."); } - } + return null; } diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java index 5b781259..c11eb6ce 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -284,7 +284,7 @@ public void onMessageRetriesDecryptionOnce() { } @Test - public void twoEventsReceivedWithIncorrectSharedSecret() { + public void twoEventsReceivedWithSecondRetryCorrect() { PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( mockInternalConnection, @@ -296,11 +296,11 @@ public void twoEventsReceivedWithIncorrectSharedSecret() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) - .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET); + .thenReturn(AUTH_RESPONSE); when(mockSecretBoxOpenerFactory.create(any())) .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) - .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))); + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET))); channel.toSubscribeMessage(); @@ -319,7 +319,46 @@ public void twoEventsReceivedWithIncorrectSharedSecret() { "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + "}\"}"); - verify(mockListener1).onDecryptionFailure("my-event", "Too many failed attempts to decrypt a previous message."); + verify(mockListener1).onEvent(argCaptor.capture()); + assertEquals("event1", argCaptor.getValue().getEventName()); + assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData()); + } + + @Test + public void twoEventsReceivedWithIncorrectSharedSecret() { + + PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( + mockInternalConnection, + getChannelName(), + mockAuthorizer, + factory, + mockSecretBoxOpenerFactory); + + when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET) + .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET); + when(mockSecretBoxOpenerFactory.create(any())) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))) + .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT))); + + channel.toSubscribeMessage(); + + PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class); + channel.bind("my-event", mockListener1); + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + // send a second message + channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" + + "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," + + "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" + + "}\"}"); + + verify(mockListener1, times(2)) + .onDecryptionFailure("my-event", "Failed to decrypt message."); } } From e694400c13717e7767b953eca7a4aa7853e8a5da Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Tue, 7 Apr 2020 14:20:52 +0100 Subject: [PATCH 43/48] Return a copy of the interestedListeners --- .../com/pusher/client/channel/impl/ChannelImpl.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java index 157fc1c2..b6f41309 100644 --- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java @@ -212,7 +212,15 @@ private void validateArguments(final String eventName, final SubscriptionEventLi protected Set getInterestedListeners(String event) { synchronized (lock) { - return eventNameToListenerMap.get(event); + + final Set sharedListeners = + eventNameToListenerMap.get(event); + + if (sharedListeners == null) { + return null; + } + + return new HashSet<>(sharedListeners); } } } From 63ca0d05e43e5bffff9f7cbd931ed362c47db039 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Tue, 7 Apr 2020 16:06:02 +0100 Subject: [PATCH 44/48] Private Encrypted Channels docs (#250) * add some Private Encrypted Channels docs * Update Private Encrypted Channels docs * Feedback to docs * Document the example app better * Feedback on docs * Delete duplicated parts of the docs --- README.md | 32 +++++++++++++++++++ .../PrivateEncryptedChannelExampleApp.java | 27 +++++++++++++--- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4bba7c46..b8aad678 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ This README covers the following topics: - [Subscribing to channels](#subscribing-to-channels) - [Public channels](#public-channels) - [Private channels](#private-channels) + - [Private encrypted channels](#private-encrypted-channels) - [Presence channels](#presence-channels) - [The User object](#the-user-object) - [Binding and handling events](#binding-and-handling-events) @@ -271,6 +272,37 @@ PrivateChannel channel = pusher.subscribePrivate("private-channel", }); ``` +### Private encrypted channels + +Similar to Private channels, you can also subscribe to a +[private encrypted channel](https://pusher.com/docs/channels/using_channels/encrypted-channels). +This library now fully supports end-to-end encryption. This means that only you and your connected clients will be able to read your messages. Pusher cannot decrypt them. + +Like the private channel, you must provide your own authentication endpoint, +with your own encryption master key. There is a +[demonstration endpoint to look at using nodejs](https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption). + +To get started you need to subscribe to your channel, provide a `PrivateEncryptedChannelEventListener`, and a list of the events you are +interested in, for example: + +```java +PrivateEncryptedChannel privateEncryptedChannel = + pusher.subscribePrivateEncrypted("private-encrypted-channel", listener, "my-event"); +``` + +In addition to the events that are possible on public channels the +`PrivateEncryptedChannelEventListener` also has the following methods: +* `onAuthenticationFailure(String message, Exception e)` - This is called if +the `Authorizer` does not successfully authenticate the subscription: +* `onDecryptionFailure(String event, String reason);` - This is called if the message cannot be +decrypted. The decryption will attempt to refresh the shared secret key once +from the `Authorizer`. + +There is a +[working example in the repo](https://github.com/pusher/pusher-websocket-java/blob/master/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java) +which you can use with the +[demonstration authorization endpoint](https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption) + ### Presence channels [Presence channels](https://pusher.com/docs/channels/using_channels/presence-channels) are private channels which provide additional events exposing who is currently subscribed to the channel. Since they extend private channels they also need to be authenticated (see [authenticating channel subscriptions](https://pusher.com/docs/channels/server_api/authenticating-users)). diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 28f4a6ff..e3d71e45 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -9,13 +9,32 @@ import com.pusher.client.connection.ConnectionStateChange; import com.pusher.client.util.HttpAuthorizer; +/* +This app demonstrates how to use Private Encrypted Channels. + +Please ensure you update this relevant parts below with your Pusher credentials before running. +and ensure you have set up an authorization endpoint with end to end encryption. Your Pusher credentials +can be found at https://dashboard.pusher.com, selecting the channels project, and visiting the App Keys +tab. + +A demonstration authorization endpoint using nodejs can be found +https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption + +For more information on private encrypted channels please read +https://pusher.com/docs/channels/using_channels/encrypted-channels + +For more pecific information on how to use private encrypted channels check out +https://github.com/pusher/pusher-websocket-java#private-encrypted-channels + */ + public class PrivateEncryptedChannelExampleApp implements ConnectionEventListener, PrivateEncryptedChannelEventListener { - private String apiKey = "FILL_ME_IN"; // "key" at https://dashboard.pusher.com + private String channelsKey = "FILL_ME_IN"; private String channelName = "private-encrypted-channel"; private String eventName = "my-event"; private String cluster = "eu"; + private String authorizationEndpoint = "http://localhost:3030/pusher/auth"; private PrivateEncryptedChannel channel; @@ -28,15 +47,15 @@ private PrivateEncryptedChannelExampleApp(final String[] args) { case 4: cluster = args[3]; case 3: eventName = args[2]; case 2: channelName = args[1]; - case 1: apiKey = args[0]; + case 1: channelsKey = args[0]; } final HttpAuthorizer authorizer = new HttpAuthorizer( - "http://localhost:3030/pusher/auth"); + authorizationEndpoint); final PusherOptions options = new PusherOptions().setAuthorizer(authorizer).setEncrypted(true); options.setCluster(cluster); - Pusher pusher = new Pusher(apiKey, options); + Pusher pusher = new Pusher(channelsKey, options); pusher.connect(this); channel = pusher.subscribePrivateEncrypted(channelName, this, eventName); From 2ec4a6dab2e7a4d479fea119cc154ad2b05e9500 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Tue, 7 Apr 2020 16:42:39 +0100 Subject: [PATCH 45/48] Add maven central badge to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b8aad678..f88b0a86 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/pusher/pusher-websocket-java.svg?branch=master)](https://travis-ci.org/pusher/pusher-websocket-java) [![codecov](https://codecov.io/gh/pusher/pusher-websocket-java/branch/master/graph/badge.svg)](https://codecov.io/gh/pusher/pusher-websocket-java) +[![Maven Central](https://img.shields.io/maven-central/v/com.pusher/pusher-java-client.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.pusher%22%20AND%20a:%22pusher-java-client%22) Pusher Channels client library for Java targeting **Android** and general Java. From 7c202e38cc69bcc72c412ad25083346a32d2cbc1 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Tue, 7 Apr 2020 16:44:00 +0100 Subject: [PATCH 46/48] Prepare 2.1.0 release --- CHANGELOG.md | 4 +++- README.md | 4 ++-- build.gradle | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3861d5c6..7753d0dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # pusher-websocket-java changelog -This Changelog is no longer being updated. For any further changes please see the Releases section on this Github repository - https://github.com/pusher/pusher-websocket-java/releases +## Version 2.1.0 - 8th April 2020 + +* Added support for [private encrypted channels](https://pusher.com/docs/channels/using_channels/encrypted-channels) ## Version 2.0.2 diff --git a/README.md b/README.md index f88b0a86..eaef968f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The pusher-java-client is available in Maven Central. com.pusher pusher-java-client - 2.0.2 + 2.1.0 ``` @@ -70,7 +70,7 @@ The pusher-java-client is available in Maven Central. ```groovy dependencies { - compile 'com.pusher:pusher-java-client:2.0.2' + compile 'com.pusher:pusher-java-client:2.1.0' } ``` diff --git a/build.gradle b/build.gradle index 85c5164e..fb8a8815 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ apply plugin: 'signing' apply plugin: 'jacoco' group = "com.pusher" -version = "2.0.2" +version = "2.1.0" sourceCompatibility = "1.8" targetCompatibility = "1.8" From b98413e805cc027b9f6053868acef533b1ad0c50 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Wed, 8 Apr 2020 11:53:58 +0100 Subject: [PATCH 47/48] Add beta notices to private encrypted channels --- README.md | 4 ++-- .../client/example/PrivateEncryptedChannelExampleApp.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eaef968f..83aaf147 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This README covers the following topics: - [Subscribing to channels](#subscribing-to-channels) - [Public channels](#public-channels) - [Private channels](#private-channels) - - [Private encrypted channels](#private-encrypted-channels) + - [Private encrypted channels [BETA]](#private-encrypted-channels) - [Presence channels](#presence-channels) - [The User object](#the-user-object) - [Binding and handling events](#binding-and-handling-events) @@ -273,7 +273,7 @@ PrivateChannel channel = pusher.subscribePrivate("private-channel", }); ``` -### Private encrypted channels +### Private encrypted channels [BETA] Similar to Private channels, you can also subscribe to a [private encrypted channel](https://pusher.com/docs/channels/using_channels/encrypted-channels). diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index e3d71e45..38acdde8 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -10,7 +10,7 @@ import com.pusher.client.util.HttpAuthorizer; /* -This app demonstrates how to use Private Encrypted Channels. +This app demonstrates how to use Private Encrypted Channels [BETA]. Please ensure you update this relevant parts below with your Pusher credentials before running. and ensure you have set up an authorization endpoint with end to end encryption. Your Pusher credentials From e311d5502a6a1682002b1d77067b3b981fdcdf27 Mon Sep 17 00:00:00 2001 From: Danielle Vass Date: Wed, 8 Apr 2020 12:06:08 +0100 Subject: [PATCH 48/48] Update comment on PrivateEncryptedChannelEventListener --- .../client/channel/PrivateEncryptedChannelEventListener.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java index 119fc58a..405f764c 100644 --- a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java +++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java @@ -2,7 +2,9 @@ /** * Interface to listen to private encrypted channel events. - * Note: This needs to extend the PrivateChannelEventListener because of the ChannelManager clearDownSubscription + * Note: This needs to extend the PrivateChannelEventListener because in the + * ChannelManager handleAuthenticationFailure we assume it's safe to cast to a + * PrivateChannelEventListener */ public interface PrivateEncryptedChannelEventListener extends PrivateChannelEventListener {