🧊 Backup and restore Ed25519 SSH keys with seed words.

  • By Charm
  • Last update: Dec 25, 2022
  • Comments: 13

Melt

Melt Mascot
Latest Release Build Status

Backup and restore SSH private keys using memorizable seed phrases.

Melt example

Installation

Use your fave package manager:

# macOS or Linux
brew install charmbracelet/tap/melt

# Arch Linux (btw)
yay -S melt-bin

# Windows (with Scoop)
scoop install melt

Or download a pre-compiled binary or package from the releases page.

Or just build it yourself (requires Go 1.17+):

git clone https://github.com/charmbracelet/melt.git
cd melt
go build ./cmd/melt/

Usage

The CLI usage looks like the following:

# Generate a seed phrase from an SSH key
melt ~/.ssh/id_ed25519

# Rebuild the key from the seed phrase
melt restore ./my-key --seed "seed phrase"

You can also pipe to and from a file:

melt ~/.ssh/id_ed25519 > words
melt restore ./recovered_id_ed25519 < words

How it Works

It all comes down to the private key seed:

Ed25519 keys start life as a 32-byte (256-bit) uniformly random binary seed (e.g. the output of SHA256 on some random input). The seed is then hashed using SHA512, which gets you 64 bytes (512 bits), which is then split into a “left half” (the first 32 bytes) and a “right half”. The left half is massaged into a curve25519 private scalar “a” by setting and clearing a few high/low-order bits. The pubkey is generated by multiplying this secret scalar by “B” (the generator), which yields a 32-byte/256-bit group element “A”.1

Knowing that, we open the key and extract its seed, and use it as entropy for the bip39 algorithm, which states:

The mnemonic must encode entropy in a multiple of 32 bits. With more entropy security is improved but the sentence length increases. We refer to the initial entropy length as ENT. The allowed size of ENT is 128-256 bits.2

Doing that, we get the mnemonic set of words back.

To restore, we:

  • get the entropy from the mnemonic
  • the entropy is effectively the key seed, so we use it to create a SSH key pair
  • the key is effectively the same that was backup up, as the key is the same. You can verify the keys by checking the public key fingerprint, which should be the same in the original and restored key.

Caveats

  • At this time, only ed25519 keys are supported.
  • If your public key has a memo (usually the user@host in which it was generated), it'll be lost. That info (or any other) can be added to the public key manually later, as it's effectively not used for signing/verifying.
  • Some bytes of your private key might change, due to their random block. The key is effectively the same though.

Feedback

We’d love to hear your thoughts on this project. Feel free to drop us a note!

License

MIT


Part of Charm.

The Charm logo

Charm热爱开源 • Charm loves open source

Footnotes

  1. Warner, Brian. How do Ed5519 keys work? (2011)

  2. Palatinus, Marek et al. Mnemonic code for generating deterministic keys (2013)

Download

melt.zip

Comments(13)

  • 1

    docs: replace go build with go install

    Changes made:

    Changed directives for building from source to use go install instead of go build so melt is added to GOPATH. Otherwise, the user would have to manually add the binary to their GOPATH

  • 2

    Support other languages

    Hi, I absolutely love the idea of backup up ssh keys with mnemonic phrases! This PR should add support for all languages supported by tyler-smith/go-bip39. The language can be specified with the flag -l or --language. The default is English.

    The following languages will be supported:

    • Chinese simplified
    • Chinese traditional
    • Czech
    • English
    • French
    • Italian
    • Japanese
    • Korean
    • Spanish
  • 3

    The formula for `melt` seem to have a wrong SHA256 hashes

    Expect

    1dc93ea4e3a2b6fa743ccf2ffd8da5871b9ff6704a68fde594a56992da5e4e8e  melt_0.4.1_Darwin_arm64.tar.gz
    383ae9761f6a822e6e56f04035000abf8e08531e032e4574eef4941de145b10c  melt_0.4.1_Darwin_x86_64.tar.gz
    85c65739858302aeded4809f008528a2bbbb8efd37f3c874dff2111968dedd4c  melt_0.4.1_linux_arm64.tar.gz
    f98510b21e88be9284312e83d8d1e893f52172015afc08fa7ec221fb9ad89ef2  melt_0.4.1_linux_x86_64.tar.gz
    

    Actual

    6fa66b54ca5e7844d21accddcc19dc0df0c6974fd0df5d044ac0afc0d17fd71e  melt_0.4.1_Darwin_arm64.tar.gz
    54c1a5d83700ef8dbd712a7530fb763004862ed6597713e5a7c3a843c0ae59c9  melt_0.4.1_Darwin_x86_64.tar.gz
    319c304770358e37199dbfd657b028c312536030bccc01daa3e3f69637fe95e7  melt_0.4.1_linux_arm64.tar.gz
    2e78883e73936234d4bbe77b3fd03efdc5f0e3b7fbc6a102ce4373fb488f76f3  melt_0.4.1_linux_x86_64.tar.gz
    

    Ref: https://github.com/charmbracelet/homebrew-tap/issues/5

  • 4

    Binary name conflict

    While packaging this for the AUR, I noticed that if the binary is called melt, it conflicts with mlt.

    Does anyone have any suggestions for an alternate name? melt-key maybe?

  • 5

    feat(deps): bump golang.org/x/text from 0.3.6 to 0.3.7

    Bumps golang.org/x/text from 0.3.6 to 0.3.7.

    Commits
    • 383b2e7 language: turn parsing panics into ErrSyntax
    • 3115f89 language: use multiple runs in TestBestMatchAlloc
    • 5c7c50e go.mod: upgrade to go 1.17
    • c2d28a6 number: match input example to be Dutch as in the output
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 6

    feat(deps): bump golang.org/x/crypto from 0.3.0 to 0.4.0

    Bumps golang.org/x/crypto from 0.3.0 to 0.4.0.

    Commits
    • eb2c406 go.mod: update golang.org/x dependencies
    • 2c47667 cryptobyte: add support for ReadASN1Integer into []byte
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 7

    feat(deps): bump golang.org/x/term from 0.2.0 to 0.3.0

    Bumps golang.org/x/term from 0.2.0 to 0.3.0.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 8

    feat(deps): bump golang.org/x/text from 0.4.0 to 0.5.0

    Bumps golang.org/x/text from 0.4.0 to 0.5.0.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 9

    feat(deps): bump golang.org/x/crypto from 0.2.0 to 0.3.0

    Bumps golang.org/x/crypto from 0.2.0 to 0.3.0.

    Commits
    • 0ec7e83 internal/wycheproof: update Go 1.20 crypto/ecdh API
    • 6fad3df ssh: support rsa-sha2-256/512 on the server side
    • 21d60a1 all: remove redundant type conversion
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 10

    feat(deps): bump golang.org/x/crypto from 0.1.0 to 0.2.0

    Bumps golang.org/x/crypto from 0.1.0 to 0.2.0.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 11

    feat(deps): bump golang.org/x/term from 0.1.0 to 0.2.0

    Bumps golang.org/x/term from 0.1.0 to 0.2.0.

    Commits
    • f72a2d8 go.mod: update golang.org/x dependencies
    • f6f2839 term: remove unused variable
    • 8365914 go.mod: update golang.org/x dependencies
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 12

    feat(deps): bump github.com/mattn/go-isatty from 0.0.16 to 0.0.17

    Bumps github.com/mattn/go-isatty from 0.0.16 to 0.0.17.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
  • 13

    Elaborate on the change of private key bytes?

    This is a fascinating tool, thank you 💕

    I was wondering, if you could elaborate (or link to further resource), explaining this caveat from the README:

    Some bytes of your private key might change, due to their random block. The key is effectively the same though.

    Is that a general property of ED25519 private keys? Or just the storage format? I'm curious.