exchange.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. // Handle in-proxy limitations. The outer client should not call exchange
  74. // in these cases in the first place, but these checks ensure we don't
  75. // export invalid payloads.
  76. //
  77. // If running in proxy-only mode, no payload is exported, since there is
  78. // not necessarily any recently successful server entry.
  79. //
  80. // If running in personal pairing tunnel, no payload is exported, since
  81. // the receiving outer client needs to be aware of and configure personal
  82. // pairing mode, but the payload is currently opaque to the outer client.
  83. if config.DisableTunnels {
  84. NoticeWarning(
  85. "ExportExchangePayload skipped due to DisableTunnels")
  86. return ""
  87. }
  88. if config.networkIDGetter.config.IsInproxyClientPersonalPairingMode() {
  89. NoticeWarning(
  90. "ExportExchangePayload skipped due to IsInproxyClientPersonalPairingMode")
  91. return ""
  92. }
  93. payload, err := exportExchangePayload(config)
  94. if err != nil {
  95. NoticeWarning("ExportExchangePayload failed: %s", errors.Trace(err))
  96. return ""
  97. }
  98. return payload
  99. }
  100. // ImportExchangePayload imports a payload generated by ExportExchangePayload.
  101. // The server entry in the payload is promoted to the affinity position so it
  102. // will be the first candidate in any establishment that begins after the
  103. // import.
  104. //
  105. // The current network ID. This may not be the same network as the exporter,
  106. // even if the client-to-client exchange occurs in real time. For example, if
  107. // the exchange is performed over NFC between two devices, they may be on
  108. // different mobile or WiFi networks. As mentioned in the comment for
  109. // ExchangedDialParameters, the exchange dial parameters includes only the
  110. // most broadly applicable fields.
  111. //
  112. // The return value indicates a successful import. If the import failed, a
  113. // a diagnostic notice has been logged.
  114. func ImportExchangePayload(config *Config, encodedPayload string) bool {
  115. err := importExchangePayload(config, encodedPayload)
  116. if err != nil {
  117. NoticeWarning("ImportExchangePayload failed: %s", errors.Trace(err))
  118. return false
  119. }
  120. return true
  121. }
  122. type exchangePayload struct {
  123. ServerEntryFields protocol.ServerEntryFields
  124. ExchangedDialParameters *ExchangedDialParameters
  125. }
  126. func exportExchangePayload(config *Config) (string, error) {
  127. networkID := config.GetNetworkID()
  128. key, err := getExchangeObfuscationKey(config)
  129. if err != nil {
  130. return "", errors.Trace(err)
  131. }
  132. serverEntryFields, dialParams, err :=
  133. GetAffinityServerEntryAndDialParameters(networkID)
  134. if err != nil {
  135. return "", errors.Trace(err)
  136. }
  137. // Fail if the server entry has no signature, as the exchange would be
  138. // insecure. Given the mechanism where handshake will return a signed server
  139. // entry to clients without one, this case is not expected to occur.
  140. if !serverEntryFields.HasSignature() {
  141. return "", errors.TraceNew("export server entry not signed")
  142. }
  143. // RemoveUnsignedFields also removes potentially sensitive local fields, so
  144. // explicitly strip these before exchanging.
  145. serverEntryFields.RemoveUnsignedFields()
  146. var exchangedDialParameters *ExchangedDialParameters
  147. if dialParams != nil {
  148. exchangedDialParameters = NewExchangedDialParameters(dialParams)
  149. }
  150. payload := &exchangePayload{
  151. ServerEntryFields: serverEntryFields,
  152. ExchangedDialParameters: exchangedDialParameters,
  153. }
  154. payloadJSON, err := json.Marshal(payload)
  155. if err != nil {
  156. return "", errors.Trace(err)
  157. }
  158. // A unique nonce is generated and included with the payload as the
  159. // obfuscation keys is not single-use.
  160. nonce, err := common.MakeSecureRandomBytes(24)
  161. if err != nil {
  162. return "", errors.Trace(err)
  163. }
  164. var secretboxNonce [24]byte
  165. copy(secretboxNonce[:], nonce)
  166. var secretboxKey [32]byte
  167. copy(secretboxKey[:], key)
  168. boxedPayload := secretbox.Seal(
  169. nil, payloadJSON, &secretboxNonce, &secretboxKey)
  170. boxedPayload = append(secretboxNonce[:], boxedPayload...)
  171. return base64.StdEncoding.EncodeToString(boxedPayload), nil
  172. }
  173. func importExchangePayload(config *Config, encodedPayload string) error {
  174. networkID := config.GetNetworkID()
  175. key, err := getExchangeObfuscationKey(config)
  176. if err != nil {
  177. return errors.Trace(err)
  178. }
  179. boxedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
  180. if err != nil {
  181. return errors.Trace(err)
  182. }
  183. if len(boxedPayload) <= 24 {
  184. return errors.TraceNew("unexpected box length")
  185. }
  186. var secretboxNonce [24]byte
  187. copy(secretboxNonce[:], boxedPayload[:24])
  188. var secretboxKey [32]byte
  189. copy(secretboxKey[:], key)
  190. payloadJSON, ok := secretbox.Open(
  191. nil, boxedPayload[24:], &secretboxNonce, &secretboxKey)
  192. if !ok {
  193. return errors.TraceNew("unbox failed")
  194. }
  195. var payload *exchangePayload
  196. err = json.Unmarshal(payloadJSON, &payload)
  197. if err != nil {
  198. return errors.Trace(err)
  199. }
  200. // Explicitly strip any unsigned fields that should not be exchanged or
  201. // imported.
  202. payload.ServerEntryFields.RemoveUnsignedFields()
  203. err = payload.ServerEntryFields.VerifySignature(
  204. config.ServerEntrySignaturePublicKey)
  205. if err != nil {
  206. return errors.Trace(err)
  207. }
  208. payload.ServerEntryFields.SetLocalSource(
  209. protocol.SERVER_ENTRY_SOURCE_EXCHANGED)
  210. payload.ServerEntryFields.SetLocalTimestamp(
  211. common.TruncateTimestampToHour(common.GetCurrentTimestamp()))
  212. // The following sequence of datastore calls -- StoreServerEntry,
  213. // PromoteServerEntry, SetDialParameters -- is not an atomic transaction but
  214. // the datastore will end up in a consistent state in case of failure to
  215. // complete the sequence. The existing calls are reused to avoid redundant
  216. // code.
  217. //
  218. // TODO: refactor existing code to allow reuse in a single transaction?
  219. err = StoreServerEntry(payload.ServerEntryFields, true)
  220. if err != nil {
  221. return errors.Trace(err)
  222. }
  223. err = PromoteServerEntry(config, payload.ServerEntryFields.GetIPAddress())
  224. if err != nil {
  225. return errors.Trace(err)
  226. }
  227. if payload.ExchangedDialParameters != nil {
  228. serverEntry, err := payload.ServerEntryFields.GetServerEntry()
  229. if err != nil {
  230. return errors.Trace(err)
  231. }
  232. // Don't abort if Validate fails, as the current client may simply not
  233. // support the exchanged dial parameter values (for example, a new tunnel
  234. // protocol).
  235. //
  236. // No notice is issued in the error case for the give linkage reason, as the
  237. // notice would be a proxy for an import success log.
  238. err = payload.ExchangedDialParameters.Validate(serverEntry)
  239. if err == nil {
  240. dialParams := payload.ExchangedDialParameters.MakeDialParameters(
  241. config,
  242. config.GetParameters().Get(),
  243. serverEntry)
  244. err = SetDialParameters(
  245. payload.ServerEntryFields.GetIPAddress(),
  246. networkID,
  247. dialParams)
  248. if err != nil {
  249. return errors.Trace(err)
  250. }
  251. }
  252. }
  253. return nil
  254. }
  255. func getExchangeObfuscationKey(config *Config) ([]byte, error) {
  256. key, err := base64.StdEncoding.DecodeString(config.ExchangeObfuscationKey)
  257. if err != nil {
  258. return nil, errors.Trace(err)
  259. }
  260. if len(key) != 32 {
  261. return nil, errors.TraceNew("invalid key size")
  262. }
  263. return key, nil
  264. }