exchange.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /*
  2. * Copyright (c) 2019, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package psiphon
  20. import (
  21. "encoding/base64"
  22. "encoding/json"
  23. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
  24. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
  25. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  26. "golang.org/x/crypto/nacl/secretbox"
  27. )
  28. // ExportExchangePayload creates a payload for client-to-client server
  29. // connection info exchange. The payload includes the most recent successful
  30. // server entry -- the server entry in the affinity position -- and any
  31. // associated dial parameters, for the current network ID.
  32. //
  33. // ExportExchangePayload is intended to be called when the client is
  34. // connected, as the affinity server will be the currently connected server
  35. // and there will be dial parameters for the current network ID.
  36. //
  37. // Only signed server entries will be exchanged. The signature is created by
  38. // the Psiphon Network and may be verified using the
  39. // ServerEntrySignaturePublicKey embedded in clients. This signture defends
  40. // against attacks by rogue clients and man-in-the-middle operatives which
  41. // could otherwise cause the importer to receive phony server entry values.
  42. //
  43. // Only a subset of dial parameters are exchanged. See the comment for
  44. // ExchangedDialParameters for more details. When no dial parameters is
  45. // present the exchange proceeds without dial parameters.
  46. //
  47. // The exchange payload is obfuscated with the ExchangeObfuscationKey embedded
  48. // in clients. The purpose of this obfuscation is to ensure that plaintext
  49. // server entry info cannot be trivially exported and displayed or published;
  50. // or at least require an effort equal to what's required without the export
  51. // feature.
  52. //
  53. // There is no success notice for exchange ExportExchangePayload (or
  54. // ImportExchangePayload) as this would potentially leak a user releationship if
  55. // two users performed and exchange and subseqently submit diagnostic feedback
  56. // containg import and export logs at almost the same point in time, along
  57. // with logs showing connections to the same server, with source "EXCHANGED"
  58. // in the importer case.
  59. //
  60. // Failure notices are logged as, presumably, the event will only appear on
  61. // one end of the exchange and the error is potentially important diagnostics.
  62. //
  63. // There remains some risk of user linkability from Connecting/ConnectedServer
  64. // diagnostics and metrics alone, because the appearance of "EXCHANGED" may
  65. // indicate an exchange event. But there are various degrees of ambiguity in
  66. // this case in terms of determining the server entry was freshly exchanged;
  67. // and with likely many users often connecting to any given server in a short
  68. // time period.
  69. //
  70. // The return value is a payload that may be exchanged with another client;
  71. // when "", the export failed and a diagnostic notice has been logged.
  72. func ExportExchangePayload(config *Config) string {
  73. payload, err := exportExchangePayload(config)
  74. if err != nil {
  75. NoticeWarning("ExportExchangePayload failed: %s", errors.Trace(err))
  76. return ""
  77. }
  78. return payload
  79. }
  80. // ImportExchangePayload imports a payload generated by ExportExchangePayload.
  81. // The server entry in the payload is promoted to the affinity position so it
  82. // will be the first candidate in any establishment that begins after the
  83. // import.
  84. //
  85. // The current network ID. This may not be the same network as the exporter,
  86. // even if the client-to-client exchange occurs in real time. For example, if
  87. // the exchange is performed over NFC between two devices, they may be on
  88. // different mobile or WiFi networks. As mentioned in the comment for
  89. // ExchangedDialParameters, the exchange dial parameters includes only the
  90. // most broadly applicable fields.
  91. //
  92. // The return value indicates a successful import. If the import failed, a
  93. // a diagnostic notice has been logged.
  94. func ImportExchangePayload(config *Config, encodedPayload string) bool {
  95. err := importExchangePayload(config, encodedPayload)
  96. if err != nil {
  97. NoticeWarning("ImportExchangePayload failed: %s", errors.Trace(err))
  98. return false
  99. }
  100. return true
  101. }
  102. type exchangePayload struct {
  103. ServerEntryFields protocol.ServerEntryFields
  104. ExchangedDialParameters *ExchangedDialParameters
  105. }
  106. func exportExchangePayload(config *Config) (string, error) {
  107. networkID := config.GetNetworkID()
  108. key, err := getExchangeObfuscationKey(config)
  109. if err != nil {
  110. return "", errors.Trace(err)
  111. }
  112. serverEntryFields, dialParams, err :=
  113. GetAffinityServerEntryAndDialParameters(networkID)
  114. if err != nil {
  115. return "", errors.Trace(err)
  116. }
  117. // Fail if the server entry has no signature, as the exchange would be
  118. // insecure. Given the mechanism where handshake will return a signed server
  119. // entry to clients without one, this case is not expected to occur.
  120. if !serverEntryFields.HasSignature() {
  121. return "", errors.TraceNew("export server entry not signed")
  122. }
  123. // RemoveUnsignedFields also removes potentially sensitive local fields, so
  124. // explicitly strip these before exchanging.
  125. serverEntryFields.RemoveUnsignedFields()
  126. var exchangedDialParameters *ExchangedDialParameters
  127. if dialParams != nil {
  128. exchangedDialParameters = NewExchangedDialParameters(dialParams)
  129. }
  130. payload := &exchangePayload{
  131. ServerEntryFields: serverEntryFields,
  132. ExchangedDialParameters: exchangedDialParameters,
  133. }
  134. payloadJSON, err := json.Marshal(payload)
  135. if err != nil {
  136. return "", errors.Trace(err)
  137. }
  138. // A unique nonce is generated and included with the payload as the
  139. // obfuscation keys is not single-use.
  140. nonce, err := common.MakeSecureRandomBytes(24)
  141. if err != nil {
  142. return "", errors.Trace(err)
  143. }
  144. var secretboxNonce [24]byte
  145. copy(secretboxNonce[:], nonce)
  146. var secretboxKey [32]byte
  147. copy(secretboxKey[:], key)
  148. boxedPayload := secretbox.Seal(
  149. nil, payloadJSON, &secretboxNonce, &secretboxKey)
  150. boxedPayload = append(secretboxNonce[:], boxedPayload...)
  151. return base64.StdEncoding.EncodeToString(boxedPayload), nil
  152. }
  153. func importExchangePayload(config *Config, encodedPayload string) error {
  154. networkID := config.GetNetworkID()
  155. key, err := getExchangeObfuscationKey(config)
  156. if err != nil {
  157. return errors.Trace(err)
  158. }
  159. boxedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
  160. if err != nil {
  161. return errors.Trace(err)
  162. }
  163. if len(boxedPayload) <= 24 {
  164. return errors.TraceNew("unexpected box length")
  165. }
  166. var secretboxNonce [24]byte
  167. copy(secretboxNonce[:], boxedPayload[:24])
  168. var secretboxKey [32]byte
  169. copy(secretboxKey[:], key)
  170. payloadJSON, ok := secretbox.Open(
  171. nil, boxedPayload[24:], &secretboxNonce, &secretboxKey)
  172. if !ok {
  173. return errors.TraceNew("unbox failed")
  174. }
  175. var payload *exchangePayload
  176. err = json.Unmarshal(payloadJSON, &payload)
  177. if err != nil {
  178. return errors.Trace(err)
  179. }
  180. // Explicitly strip any unsigned fields that should not be exchanged or
  181. // imported.
  182. payload.ServerEntryFields.RemoveUnsignedFields()
  183. err = payload.ServerEntryFields.VerifySignature(
  184. config.ServerEntrySignaturePublicKey)
  185. if err != nil {
  186. return errors.Trace(err)
  187. }
  188. payload.ServerEntryFields.SetLocalSource(
  189. protocol.SERVER_ENTRY_SOURCE_EXCHANGED)
  190. payload.ServerEntryFields.SetLocalTimestamp(
  191. common.TruncateTimestampToHour(common.GetCurrentTimestamp()))
  192. // The following sequence of datastore calls -- StoreServerEntry,
  193. // PromoteServerEntry, SetDialParameters -- is not an atomic transaction but
  194. // the datastore will end up in a consistent state in case of failure to
  195. // complete the sequence. The existing calls are reused to avoid redundant
  196. // code.
  197. //
  198. // TODO: refactor existing code to allow reuse in a single transaction?
  199. err = StoreServerEntry(payload.ServerEntryFields, true)
  200. if err != nil {
  201. return errors.Trace(err)
  202. }
  203. err = PromoteServerEntry(config, payload.ServerEntryFields.GetIPAddress())
  204. if err != nil {
  205. return errors.Trace(err)
  206. }
  207. if payload.ExchangedDialParameters != nil {
  208. serverEntry, err := payload.ServerEntryFields.GetServerEntry()
  209. if err != nil {
  210. return errors.Trace(err)
  211. }
  212. // Don't abort if Validate fails, as the current client may simply not
  213. // support the exchanged dial parameter values (for example, a new tunnel
  214. // protocol).
  215. //
  216. // No notice is issued in the error case for the give linkage reason, as the
  217. // notice would be a proxy for an import success log.
  218. err = payload.ExchangedDialParameters.Validate(serverEntry)
  219. if err == nil {
  220. dialParams := payload.ExchangedDialParameters.MakeDialParameters(
  221. config,
  222. config.GetParameters().Get(),
  223. serverEntry)
  224. err = SetDialParameters(
  225. payload.ServerEntryFields.GetIPAddress(),
  226. networkID,
  227. dialParams)
  228. if err != nil {
  229. return errors.Trace(err)
  230. }
  231. }
  232. }
  233. return nil
  234. }
  235. func getExchangeObfuscationKey(config *Config) ([]byte, error) {
  236. key, err := base64.StdEncoding.DecodeString(config.ExchangeObfuscationKey)
  237. if err != nil {
  238. return nil, errors.Trace(err)
  239. }
  240. if len(key) != 32 {
  241. return nil, errors.TraceNew("invalid key size")
  242. }
  243. return key, nil
  244. }