Skip to content

Instantly share code, notes, and snippets.

@djspiewak
Last active August 29, 2015 14:11
Show Gist options
  • Save djspiewak/d8082be3efeb875acd4d to your computer and use it in GitHub Desktop.
Save djspiewak/d8082be3efeb875acd4d to your computer and use it in GitHub Desktop.

Configuring SBT to use 1Password

  1. Install swig-python
  2. Use pip to install 1pass
  3. Install py-levenshtein to avoid annoying warnings
  4. Locate the nearest 1pass script (note: it may be behind you)
  5. Test 1pass on a random password

Create ~/.sbt/0.13/plugins/build.sbt with the following contents:

sbtPlugin := true

Create ~/.sbt/0.13/plugins/OnePassword.scala and enter the following contents:

import sbt._

import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets

object OnePassword extends AutoPlugin {

  object autoImport {
	val isSbtBuild = settingKey[Boolean]("Detects SBT meta projects")
  }

  // alright, let's be real here: this is a security leak. sbt-pgp has the same flaw
  private[this] lazy val password =
	SimpleReader.readLine("1Password Master Password: ", Some('*')) getOrElse error("Must provide a password")

  def apply(zone: String, host: String, user: String, entry: String)(enabled: Boolean): Seq[Credentials] = {
	if (enabled) {
	  val onePass = Process("/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin/1pass" :: "--no-prompt" :: entry :: Nil)
	  val is = new ByteArrayInputStream(password.getBytes(StandardCharsets.UTF_8))

	  val lines = (onePass #< is).lines

	  Seq(Credentials(
		zone,
		host,
		user,
		lines.last))
	} else {
	  Seq()
	}
  }
}

Pay no mind to the dangerous security leak... If it makes you feel better, sbt-pgp leaks credentials in exactly the same way! Of course, your PGP key password isn't quite as sensitive as your 1Password master password, so maybe you shouldn't feel better after all. I have an issue open to get this problem addressed in SBT itself, but it probably won't happen before 0.14.

Create ~/.sbt/0.13/onepass.sbt with the following contents:

import OnePassword.autoImport._

isSbtBuild :=
  Keys.sbtPlugin.?.value.getOrElse(false) &&
	(Keys.baseDirectory in ThisProject).value.getName == "project"

You can name this file anything you like. It just has to exist somewhere in the 0.13 directory.

To configure a set of credentials with a password stored in 1Password, follow this pattern (here is my Sonatype credential entry):

import OnePassword.autoImport._

credentials ++=
  OnePassword(
	"Sonatype Nexus Repository Manager",
	"oss.sonatype.org",
	"djspiewak",
	"Sonatype")(!isSbtBuild.value)

The "Sonatype" final parameter refers to the name of the 1Password entry which contains my Sonatype password. At present, I do not have a way to derive the username from 1Password, though in theory this is possible.

The isSbtBuild setting is introduced by this plugin detects whether or not the enclosing SBT context represents one of the many meta-builds which bootstrap SBT itself. Using this setting ensures that we only prompt for the master password when it is required for a task within your specific build (e.g. update or publish). If you find yourself needing this credential set for meta-build resolution (e.g. if you have a custom plugin that is stored in a Nexus protected by your credentials), you will need to replace the credentials line with something like the following:

credentials ++=
  OnePassword(
	"Sonatype Nexus Repository Manager",
	"oss.sonatype.org",
	"djspiewak",
	"Sonatype")(true)

This will force you to enter your master password quite a bit though, so I don't recommend going this route unless you absolutely have to.

Troubleshooting

I've noticed that 1pass, and more specifically Python's json library, has trouble parsing older 1Password items that contain forward slashes (/). I'm not 100% certain of why this is, but it happens. You'll see this manifest in the form of an error that looks like this:

1pass: Error: Extra data: line 1 column ...

If you see this error, open up the 1Password item you're trying to load and retype any forward slashes. Chances are, these are in the URL associated with the item. Just edit it, delete the slashes and put them in again. Save the item and you're back in business. Note that it may be possible to replicate this effect by simply editing and then saving the item, but I didn't try that.

Ways Around the Leak

One stupidly obvious way around the leak is to just obfuscate the password at rest in memory. This sounds like a funky idea, but it actually does a passable job of addressing the primary threat vector here, which is someone crafting a build that reflectively peaks into the OnePassword.password field and exfiltrates the contents. This exfiltration is only useful if the data is relatively easy to reverse. A simple obfuscation of the password with a hard-coded key (i.e. not stored in a variable) should force some more significant labor involving classloaders or plain old file IO and parsing. Not impossible, but harder. If each person who uses this plugin hard-codes their own custom key, that would put a hefty speed-bump in the way of any wide scale attack.

Another way around the leak is to deactivate the caching altogether. This is relatively easy to do (just change the lazy val to a def), but unfortunately it does require you to reenter your master password once for every single credentials entry backed by 1Password, and that gets annoying to say the least. Pick your poison.

As I mentioned, I've opened an issue on sbt-pgp that proposes the addition of a SecureCache to SBT, which would neatly resolve this issue and make it very difficult (but far from impossible!) for a malicious build or plugin to extract your master password. Hopefully this solution gets implemented with all speed.

Special Thanks

Josh Suereth is awesome. That is all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment