Skip to content

Instantly share code, notes, and snippets.

@djspiewak
Last active August 29, 2015 14:11

Revisions

  1. djspiewak revised this gist Dec 16, 2014. 1 changed file with 62 additions and 21 deletions.
    83 changes: 62 additions & 21 deletions sbt-1password.md
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,12 @@
    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:

    ```sbt
    sbtPlugin := true
    ```

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

    ```scala
    @@ -14,43 +20,76 @@ import sbt._
    import java.io.ByteArrayInputStream
    import java.nio.charset.StandardCharsets

    object OnePassword {
    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): Credentials = {
    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

    Credentials(
    zone,
    host,
    user,
    lines.last) // throw an exception if it doesn't work
    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.
    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](https://github.com/sbt/sbt-pgp/issues/68) 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:

    ```sbt
    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):

    ```sbt
    credentials +=
    import OnePassword.autoImport._

    credentials ++=
    OnePassword(
    "Sonatype Nexus Repository Manager",
    "oss.sonatype.org",
    "djspiewak",
    "Sonatype")
    "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.

    At present, you are prompted for your master password *on startup* and on the first time you `publish`. Ideally, it would only be at the latter time, but I honestly don't know how to do that with SBT plugins. Help appreciated!
    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:

    ```sbt
    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

    @@ -62,12 +101,14 @@ I've noticed that `1pass`, and more specifically Python's `json` library, has tr

    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.

    ## Annoyances

    Guess what! If you put your credentials in the global settings, they get loaded not just when they're needed by your build, but also when SBT bootstraps itself. This means that you will need to enter your master password when SBT launches (any SBT instance, not just those using your credentials), and then *again* when you need to publish something. Though, if you publish repeatedly, I believe the password should be cached (hence the security leak). The point being that the completely unnecessary prompting for your password during the bootstrap is really, *really* annoying and I'd like to bypass it if at all possible, but I don't know how yet.

    ## 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.
    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.
  2. djspiewak revised this gist Dec 15, 2014. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions sbt-1password.md
    Original file line number Diff line number Diff line change
    @@ -65,3 +65,9 @@ If you see this error, open up the 1Password item you're trying to load and *ret
    ## Annoyances

    Guess what! If you put your credentials in the global settings, they get loaded not just when they're needed by your build, but also when SBT bootstraps itself. This means that you will need to enter your master password when SBT launches (any SBT instance, not just those using your credentials), and then *again* when you need to publish something. Though, if you publish repeatedly, I believe the password should be cached (hence the security leak). The point being that the completely unnecessary prompting for your password during the bootstrap is really, *really* annoying and I'd like to bypass it if at all possible, but I don't know how yet.

    ## 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.
  3. djspiewak revised this gist Dec 15, 2014. 1 changed file with 16 additions and 2 deletions.
    18 changes: 16 additions & 2 deletions sbt-1password.md
    Original file line number Diff line number Diff line change
    @@ -35,7 +35,7 @@ object OnePassword {
    }
    ```

    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 should feel better after all.
    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.

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

    @@ -50,4 +50,18 @@ credentials +=

    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.

    At present, you are prompted for your master password *on startup* and on the first time you `publish`. Ideally, it would only be at the latter time, but I honestly don't know how to do that with SBT plugins. Help appreciated!
    At present, you are prompted for your master password *on startup* and on the first time you `publish`. Ideally, it would only be at the latter time, but I honestly don't know how to do that with SBT plugins. Help appreciated!

    ## 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.

    ## Annoyances

    Guess what! If you put your credentials in the global settings, they get loaded not just when they're needed by your build, but also when SBT bootstraps itself. This means that you will need to enter your master password when SBT launches (any SBT instance, not just those using your credentials), and then *again* when you need to publish something. Though, if you publish repeatedly, I believe the password should be cached (hence the security leak). The point being that the completely unnecessary prompting for your password during the bootstrap is really, *really* annoying and I'd like to bypass it if at all possible, but I don't know how yet.
  4. djspiewak created this gist Dec 15, 2014.
    53 changes: 53 additions & 0 deletions sbt-1password.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    # 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/OnePassword.scala` and enter the following contents:

    ```scala
    import sbt._

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

    object OnePassword {

    // 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): Credentials = {
    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

    Credentials(
    zone,
    host,
    user,
    lines.last) // throw an exception if it doesn't work
    }
    }
    ```

    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 should feel better after all.

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

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

    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.

    At present, you are prompted for your master password *on startup* and on the first time you `publish`. Ideally, it would only be at the latter time, but I honestly don't know how to do that with SBT plugins. Help appreciated!