|
| 1 | +package transports |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "crypto/aes" |
| 6 | + "crypto/cipher" |
| 7 | + "crypto/rand" |
| 8 | + "crypto/sha256" |
| 9 | + "errors" |
| 10 | + "fmt" |
| 11 | + "strconv" |
| 12 | + |
| 13 | + "github.com/refraction-networking/gotapdance/ed25519/extra25519" |
| 14 | + "golang.org/x/crypto/curve25519" |
| 15 | +) |
| 16 | + |
| 17 | +// Obfuscator provides an interface for obfuscating the tags that are sent by transports in order to |
| 18 | +// indicate their knowledge of the shared secret to the station. |
| 19 | +type Obfuscator interface { |
| 20 | + // Take the plain text and perform an obfuscation to make it distinguishable to the station |
| 21 | + Obfuscate(plaintext []byte, stationPubkey []byte) ([]byte, error) |
| 22 | + |
| 23 | + // Take a cipher text and de-obfuscate to make it usable by the station |
| 24 | + TryReveal(cipherText []byte, privateKey [32]byte) ([]byte, error) |
| 25 | +} |
| 26 | + |
| 27 | +// GCMObfuscator implements the Obfuscator interface using ECDHE and AES GCM. Prevents tag re-use. |
| 28 | +type GCMObfuscator struct{} |
| 29 | + |
| 30 | +// TryReveal for GCMObfuscator expects a ciphertext object where the first 32 bytes is an elligator |
| 31 | +// encoded public key with which the server can derive an ECDHE shared secret. This secret is then |
| 32 | +// used to decrypt and authenticate the remainder of the plaintext using AES GCM. |
| 33 | +func (GCMObfuscator) TryReveal(ciphertext []byte, privateKey [32]byte) ([]byte, error) { |
| 34 | + if len(ciphertext) < 48 { |
| 35 | + return nil, ErrPublicKeyLen |
| 36 | + } |
| 37 | + |
| 38 | + var representative, clientPubkey [32]byte |
| 39 | + copy(representative[:], ciphertext[:32]) |
| 40 | + representative[31] &= 0x3F |
| 41 | + extra25519.RepresentativeToPublicKey(&clientPubkey, &representative) |
| 42 | + |
| 43 | + sharedSecret, err := curve25519.X25519(privateKey[:], clientPubkey[:]) |
| 44 | + if err != nil { |
| 45 | + return nil, err |
| 46 | + } |
| 47 | + |
| 48 | + stationPubkeyHash := sha256.Sum256(sharedSecret[:]) |
| 49 | + aesKey := stationPubkeyHash[:16] |
| 50 | + aesIvTag := stationPubkeyHash[16:28] |
| 51 | + |
| 52 | + block, err := aes.NewCipher(aesKey) |
| 53 | + if err != nil { |
| 54 | + return nil, err |
| 55 | + } |
| 56 | + // return block.Decrypt(nil, aesIvTag, cipherText[32:]) |
| 57 | + |
| 58 | + aesgcm, err := cipher.NewGCM(block) |
| 59 | + if err != nil { |
| 60 | + return nil, err |
| 61 | + } |
| 62 | + |
| 63 | + return aesgcm.Open(nil, aesIvTag, ciphertext[32:], nil) |
| 64 | +} |
| 65 | + |
| 66 | +// Obfuscate for GCMObfuscator derives a shared key using ECDHE an then encrypts the plaintext under |
| 67 | +// that key using AES GCM. The elligator representative for the clients public key is prepended |
| 68 | +// to the returned byte array. This means that the result length will likely be: |
| 69 | +// |
| 70 | +// 32 + len(plaintext) + 16 |
| 71 | +// |
| 72 | +// [elligator encoded client Pub][Ciphertext + Auth tag] |
| 73 | +func (GCMObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) { |
| 74 | + if len(stationPubkey) != 32 { |
| 75 | + return nil, fmt.Errorf("%w, received: %d", ErrPublicKeyLen, len(stationPubkey)) |
| 76 | + } |
| 77 | + var clientPrivate, clientPublic, representative [32]byte |
| 78 | + var sharedSecret []byte |
| 79 | + for ok := false; !ok; { |
| 80 | + var sliceKeyPrivate []byte = clientPrivate[:] |
| 81 | + _, err := rand.Read(sliceKeyPrivate) |
| 82 | + if err != nil { |
| 83 | + return nil, err |
| 84 | + } |
| 85 | + |
| 86 | + ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate) |
| 87 | + } |
| 88 | + |
| 89 | + sharedSecret, err := curve25519.X25519(clientPrivate[:], stationPubkey) |
| 90 | + if err != nil { |
| 91 | + return nil, err |
| 92 | + } |
| 93 | + |
| 94 | + // extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?) |
| 95 | + // Other implementations of elligator may have up to 2 non-random bits. |
| 96 | + // Here we randomize the bit, expecting it to be flipped back to 0 on station |
| 97 | + randByte := make([]byte, 1) |
| 98 | + _, err = rand.Read(randByte) |
| 99 | + if err != nil { |
| 100 | + return nil, err |
| 101 | + } |
| 102 | + representative[31] |= (0xC0 & randByte[0]) |
| 103 | + |
| 104 | + tagBuf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES |
| 105 | + tagBuf.Write(representative[:]) |
| 106 | + |
| 107 | + stationPubkeyHash := sha256.Sum256(sharedSecret[:]) |
| 108 | + aesKey := stationPubkeyHash[:16] |
| 109 | + aesIvTag := stationPubkeyHash[16:28] // 12 bytes for plaintext nonce |
| 110 | + |
| 111 | + cipherText, err := aesGcmEncrypt(plainText, aesKey, aesIvTag) |
| 112 | + if err != nil { |
| 113 | + return nil, err |
| 114 | + } |
| 115 | + |
| 116 | + tagBuf.Write(cipherText) |
| 117 | + tag := tagBuf.Bytes() |
| 118 | + |
| 119 | + return tag, nil |
| 120 | +} |
| 121 | + |
| 122 | +// CTRObfuscator implements the Obfuscator interface using ECDHE and AES CTR. Prevents tag re-use. |
| 123 | +type CTRObfuscator struct{} |
| 124 | + |
| 125 | +// TryReveal for CTRObfuscator expects a ciphertext object where the first 32 bytes is an elligator |
| 126 | +// encoded public key with which the server can derive an ECDHE shared secret. This secret is then |
| 127 | +// used to decrypt the remainder of the plaintext using AES CTR. |
| 128 | +func (CTRObfuscator) TryReveal(ciphertext []byte, privateKey [32]byte) ([]byte, error) { |
| 129 | + if len(ciphertext) < 32 { |
| 130 | + return nil, ErrPublicKeyLen |
| 131 | + } |
| 132 | + |
| 133 | + var representative, clientPubkey [32]byte |
| 134 | + copy(representative[:], ciphertext[:32]) |
| 135 | + representative[31] &= 0x3F |
| 136 | + extra25519.RepresentativeToPublicKey(&clientPubkey, &representative) |
| 137 | + |
| 138 | + sharedSecret, err := curve25519.X25519(privateKey[:], clientPubkey[:]) |
| 139 | + if err != nil { |
| 140 | + return nil, err |
| 141 | + } |
| 142 | + |
| 143 | + stationPubkeyHash := sha256.Sum256(sharedSecret[:]) |
| 144 | + aesKey := stationPubkeyHash[:16] |
| 145 | + aesIvTag := stationPubkeyHash[16:32] |
| 146 | + |
| 147 | + return aesCTR(ciphertext[32:], aesKey, aesIvTag) |
| 148 | +} |
| 149 | + |
| 150 | +// Obfuscate for CTRObfuscator derives a shared key using ECDHE an then encrypts the plaintext under |
| 151 | +// that key using AES CTR. The elligator representative for the clients public key is prepended |
| 152 | +// to the returned byte array. This means that the result length will likely be: |
| 153 | +// |
| 154 | +// 32 + len(plaintext) |
| 155 | +// |
| 156 | +// [elligator encoded client Pub][Ciphertext] |
| 157 | +func (CTRObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) { |
| 158 | + if len(stationPubkey) != 32 { |
| 159 | + return nil, errors.New("Unexpected station pubkey length. Expected: 32." + |
| 160 | + " Received: " + strconv.Itoa(len(stationPubkey)) + ".") |
| 161 | + } |
| 162 | + var clientPrivate, clientPublic, representative [32]byte |
| 163 | + var sharedSecret []byte |
| 164 | + for ok := false; !ok; { |
| 165 | + var sliceKeyPrivate []byte = clientPrivate[:] |
| 166 | + _, err := rand.Read(sliceKeyPrivate) |
| 167 | + if err != nil { |
| 168 | + return nil, err |
| 169 | + } |
| 170 | + |
| 171 | + ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate) |
| 172 | + } |
| 173 | + |
| 174 | + sharedSecret, err := curve25519.X25519(clientPrivate[:], stationPubkey) |
| 175 | + if err != nil { |
| 176 | + return nil, err |
| 177 | + } |
| 178 | + |
| 179 | + // extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?) |
| 180 | + // Other implementations of elligator may have up to 2 non-random bits. |
| 181 | + // Here we randomize the bit, expecting it to be flipped back to 0 on station |
| 182 | + randByte := make([]byte, 1) |
| 183 | + _, err = rand.Read(randByte) |
| 184 | + if err != nil { |
| 185 | + return nil, err |
| 186 | + } |
| 187 | + representative[31] |= (0xC0 & randByte[0]) |
| 188 | + |
| 189 | + tagBuf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES |
| 190 | + tagBuf.Write(representative[:]) |
| 191 | + |
| 192 | + stationPubkeyHash := sha256.Sum256(sharedSecret[:]) |
| 193 | + aesKey := stationPubkeyHash[:16] |
| 194 | + aesIvTag := stationPubkeyHash[16:32] // 16 bytes for CTR IV |
| 195 | + |
| 196 | + cipherText, err := aesCTR(plainText, aesKey, aesIvTag) |
| 197 | + if err != nil { |
| 198 | + return nil, err |
| 199 | + } |
| 200 | + |
| 201 | + tagBuf.Write(cipherText) |
| 202 | + tag := tagBuf.Bytes() |
| 203 | + |
| 204 | + return tag, nil |
| 205 | +} |
| 206 | + |
| 207 | +// NilObfuscator implements the Obfuscator interface for no modification the provided tag / |
| 208 | +// plaintext / ciphertext. Will NOT prevent tag re-use if a registration is re-used. |
| 209 | +type NilObfuscator struct{} |
| 210 | + |
| 211 | +// TryReveal for NilObfuscator just returns the provided ciphertext without modification |
| 212 | +func (NilObfuscator) TryReveal(cipherText []byte, privateKey [32]byte) ([]byte, error) { |
| 213 | + return cipherText, nil |
| 214 | +} |
| 215 | + |
| 216 | +// Obfuscate for NilObfuscator just returns the provided plaintext without modification |
| 217 | +func (NilObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) { |
| 218 | + return plainText, nil |
| 219 | +} |
| 220 | + |
| 221 | +// The key argument should be the AES key, either 16 or 32 bytes |
| 222 | +// to select AES-128 or AES-256. |
| 223 | +func aesGcmEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) { |
| 224 | + block, err := aes.NewCipher(key) |
| 225 | + if err != nil { |
| 226 | + return nil, err |
| 227 | + } |
| 228 | + |
| 229 | + aesGcmCipher, err := cipher.NewGCM(block) |
| 230 | + if err != nil { |
| 231 | + return nil, err |
| 232 | + } |
| 233 | + return aesGcmCipher.Seal(nil, iv, plaintext, nil), nil |
| 234 | +} |
| 235 | + |
| 236 | +func aesCTR(in []byte, key []byte, iv []byte) ([]byte, error) { |
| 237 | + block, err := aes.NewCipher(key) |
| 238 | + if err != nil { |
| 239 | + return nil, err |
| 240 | + } |
| 241 | + |
| 242 | + out := make([]byte, len(in)) |
| 243 | + stream := cipher.NewCTR(block, iv) |
| 244 | + stream.XORKeyStream(out, in) |
| 245 | + |
| 246 | + return out, nil |
| 247 | +} |
0 commit comments