wifi: allow setclient to join hidden networks

By default, wpa_supplicant doesn't scan SSIDs with specific Probe
Request frames - it only uses a broadcast SSID.

This makes it impossible to join hidden networks. Fix this, since our
setup network will usually be hidden and our primary networks may be
hidden by customers at will.

Also, remove dependency on `wpa_passphrase` and start computing the
passphrase in Python. This avoids complicated string edits.

Change-Id: Ia0bd19c20a0a04f1cc030e33eae32aca363c3908
diff --git a/wifi/configs.py b/wifi/configs.py
index 676182a..3377a08 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -4,6 +4,8 @@
 
 import subprocess
 
+import Crypto.Protocol.KDF
+
 import experiment
 import utils
 
@@ -133,12 +135,6 @@
 wpa_ptk_rekey=0
 """
 
-_WPA_SUPPLICANT_UNSECURED_TPL = """network={{
-\tssid="{ssid}"
-\tkey_mgmt=NONE
-}}
-"""
-
 
 def generate_hostapd_config(
     phy_info, interface, band, channel, width, protocols, psk, opt):
@@ -327,22 +323,55 @@
   return '\n'.join(hostapd_conf_parts)
 
 
+def make_network_block(network_block_lines):
+  return 'network={\n%s\n}\n' % '\n'.join(network_block_lines)
+
+
+def open_network_lines(ssid):
+  return ['\tssid="%s"' % utils.sanitize_ssid(ssid),
+          '\tkey_mgmt=NONE']
+
+
+def wpa_network_lines(ssid, passphrase):
+  """Like `wpa_passphrase "$ssid" "$passphrase"`, but more convenient output.
+
+  This generates raw config lines, so we can update the config when the defaults
+  don't make sense for us without doing parsing.
+
+  N.b. wpa_passphrase double quotes provided SSID and passphrase arguments, and
+  does not escape quotes or backslashes.
+
+  Args:
+    ssid: a wifi network SSID
+    passphrase: a wifi network PSK
+  Returns:
+    lines of a network block that will let wpa_supplicant join this network
+  """
+  clean_ssid = utils.sanitize_ssid(ssid)
+  network_lines = ['\tssid="%s"' % clean_ssid]
+  clean_passphrase = utils.validate_and_sanitize_psk(passphrase)
+  if len(clean_passphrase) == 64:
+    network_lines += ['\tpsk=%s' % clean_passphrase]
+  else:
+    raw_psk = Crypto.Protocol.KDF.PBKDF2(clean_passphrase, clean_ssid, 32, 4096)
+    hex_psk = ''.join(ch.encode('hex') for ch in raw_psk)
+    network_lines += ['\t#psk="%s"' % clean_passphrase, '\tpsk=%s' % hex_psk]
+
+  return network_lines
+
+
 def generate_wpa_supplicant_config(ssid, passphrase, opt):
   """Generate a wpa_supplicant config from the provided arguments."""
-
-  if passphrase is not None:
-    network_block = subprocess.check_output(
-        ('wpa_passphrase',
-         utils.sanitize_ssid(ssid),
-         utils.validate_and_sanitize_psk(passphrase)))
+  if passphrase is None:
+    network_block_lines = open_network_lines(ssid)
   else:
-    network_block = _WPA_SUPPLICANT_UNSECURED_TPL.format(ssid=ssid)
+    network_block_lines = wpa_network_lines(ssid, passphrase)
 
+  network_block_lines.append('\tscan_ssid=1')
   if opt.bssid:
-    network_block_lines = network_block.splitlines(True)
-    network_block_lines[-1:-1] = ['\tbssid=%s\n' %
-                                  utils.validate_and_sanitize_bssid(opt.bssid)]
-    network_block = ''.join(network_block_lines)
+    network_block_lines.append('\tbssid=%s' %
+                               utils.validate_and_sanitize_bssid(opt.bssid))
+  network_block = make_network_block(network_block_lines)
 
   lines = [
       'ctrl_interface=/var/run/wpa_supplicant',
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 684c45f..ab5d6c7 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python -S
+#!/usr/bin/python
 
 """Tests for configs.py."""
 
@@ -17,6 +17,7 @@
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+\tscan_ssid=1
 }
 """
 
@@ -27,6 +28,7 @@
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+\tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
 }
 """
@@ -39,6 +41,7 @@
 network={
 \tssid="some ssid"
 \tkey_mgmt=NONE
+\tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
 }
 """
@@ -395,6 +398,47 @@
     self.supports_provisioning = False
 
 
+def wpa_passphrase(ssid, passphrase):
+  return configs.make_network_block(
+      configs.wpa_network_lines(ssid, passphrase))
+
+
+def wpa_passphrase_subprocess(ssid, passphrase):
+  return subprocess.check_output(
+      ('wpa_passphrase',
+       utils.sanitize_ssid(ssid),
+       utils.validate_and_sanitize_psk(passphrase)))
+
+
+@wvtest.wvtest
+def wpa_passphrase_test():
+  """Make sure the configs we generate are the same as wpa_passphrase."""
+  for testdata in (
+      ('some ssid', 'some passphrase'),
+      (r'some\ssid', 'some passphrase'),
+      ('some ssid', r'some\passphrase'),
+      ('some"ssid', 'some passphrase'),
+      ('some ssid', 'some"passphrase')):
+    got, want = wpa_passphrase(*testdata), wpa_passphrase_subprocess(*testdata)
+    wvtest.WVPASSEQ(got, want)
+
+
+@wvtest.wvtest
+def wpa_raw_psk_test():
+  """Make sure we do the right thing when we get a raw PSK too."""
+  ssid = 'some ssid'
+  psk = '41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f'
+
+  got = wpa_passphrase(ssid, psk)
+  # hard code since `wpa_passphrase` dies if given a 64-character hex psk
+  want = """network={
+\tssid="some ssid"
+\tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
+}
+"""
+  wvtest.WVPASSEQ(got, want)
+
+
 # pylint: disable=protected-access
 @wvtest.wvtest
 def generate_hostapd_config_test():
diff --git a/wifi/wifi_test.py b/wifi/wifi_test.py
index 10c01ad..7b5f3f4 100755
--- a/wifi/wifi_test.py
+++ b/wifi/wifi_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python -S
+#!/usr/bin/python
 
 """Tests for wifi.py."""