diff --git a/cmd/melt/main.go b/cmd/melt/main.go index 59e90ae..4bf53cc 100644 --- a/cmd/melt/main.go +++ b/cmd/melt/main.go @@ -18,6 +18,8 @@ import ( "github.com/muesli/reflow/wordwrap" "github.com/muesli/roff" "github.com/muesli/termenv" + "github.com/tyler-smith/go-bip39" + "github.com/tyler-smith/go-bip39/wordlists" "golang.org/x/crypto/ssh" "golang.org/x/term" ) @@ -39,6 +41,9 @@ var ( Padding(1, 2) keyPathStyle = lipgloss.NewStyle().Foreground(violet) + mnemonic string + language string + rootCmd = &coral.Command{ Use: "melt", Example: ` melt ~/.ssh/id_ed25519 @@ -51,6 +56,10 @@ be used to rebuild your public and private keys.`, Args: coral.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *coral.Command, args []string) error { + if err := setLanguage(language); err != nil { + return err + } + mnemonic, err := backup(args[0], nil) if err != nil { return err @@ -67,8 +76,12 @@ be used to rebuild your public and private keys.`, // Build formatted restore command const cmdEOL = " \\" + var lang string + if language != "en" { + lang = fmt.Sprintf(" --language %s", language) + } cmd := wordwrap.String( - os.Args[0]+` restore ./my-key --seed "`+mnemonic+`"`, + os.Args[0]+` restore`+lang+` ./my-key --seed "`+mnemonic+`"`, w-lipgloss.Width(cmdEOL)-baseStyle.GetHorizontalFrameSize()*2, ) leftPad := strings.Repeat(" ", baseStyle.GetMarginLeft()) @@ -91,7 +104,6 @@ be used to rebuild your public and private keys.`, }, } - mnemonic string restoreCmd = &coral.Command{ Use: "restore", Short: "Recreate a key using the given seed phrase", @@ -100,6 +112,10 @@ be used to rebuild your public and private keys.`, Aliases: []string{"res", "r"}, Args: coral.ExactArgs(1), RunE: func(cmd *coral.Command, args []string) error { + if err := setLanguage(language); err != nil { + return err + } + if err := restore(maybeFile(mnemonic), args[0]); err != nil { return err } @@ -131,6 +147,7 @@ be used to rebuild your public and private keys.`, ) func init() { + rootCmd.PersistentFlags().StringVarP(&language, "language", "l", "en", "Language") rootCmd.AddCommand(restoreCmd, manCmd) restoreCmd.PersistentFlags().StringVarP(&mnemonic, "seed", "s", "-", "Seed phrase") @@ -238,3 +255,30 @@ func completeColor(truecolor, ansi256, ansi string) string { } return ansi } + +// setLanguage sets the language of the big39 mnemonic seed. +func setLanguage(language string) error { + switch strings.ToLower(language) { + case "chinese-simplified", "zh", "zh_HANS": + bip39.SetWordList(wordlists.ChineseSimplified) + case "chinese-traditional", "zh_HANT": + bip39.SetWordList(wordlists.ChineseTraditional) + case "czech", "cs": + bip39.SetWordList(wordlists.Czech) + case "english", "en": + bip39.SetWordList(wordlists.English) + case "french", "fr": + bip39.SetWordList(wordlists.French) + case "italian", "it": + bip39.SetWordList(wordlists.Italian) + case "japanese", "ja": + bip39.SetWordList(wordlists.Japanese) + case "korean", "ko": + bip39.SetWordList(wordlists.Korean) + case "spanish", "es": + bip39.SetWordList(wordlists.Spanish) + default: + return fmt.Errorf("this language is not supported") + } + return nil +} diff --git a/cmd/melt/main_test.go b/cmd/melt/main_test.go index 6801afd..210e419 100644 --- a/cmd/melt/main_test.go +++ b/cmd/melt/main_test.go @@ -76,6 +76,47 @@ func TestBackupRestoreKnownKey(t *testing.T) { }) } +func TestBackupRestoreKnownKeyInJapanse(t *testing.T) { + const expectedMnemonic = ` + いきおい ざるそば えもの せんめんじょ てあみ ていねい はったつ + ろこつ すあし のぞく かまう ほくろ らくご けぶかい たおす よゆう + ひめじし くたびれる ぐんたい なわばり にかい えほん せなか + そいとげる + ` + const expectedSum = "ba34175ef608633b29f046b40cce596dd221347b77abba40763eef2e7ae51fe9" + const expectedFingerprint = "SHA256:tX0ZrsNLIB/ZlRK3vy/HsWIIkyBNhYhCSGmtqtxJcWo" + + // set language to Japanse + setLanguage("japanese") + + // set language back to English + t.Cleanup(func() { + setLanguage("english") + }) + + t.Run("backup", func(t *testing.T) { + mnemonic, err := backup("testdata/id_ed25519", nil) + is := is.New(t) + is.NoErr(err) + is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " ")) + }) + + t.Run("restore", func(t *testing.T) { + is := is.New(t) + path := filepath.Join(t.TempDir(), "key") + is.NoErr(restore(expectedMnemonic, path)) + is.Equal(expectedSum, sha256sum(t, path+".pub")) + + bts, err := os.ReadFile(path) + is.NoErr(err) + + k, err := ssh.ParsePrivateKey(bts) + is.NoErr(err) + + is.Equal(expectedFingerprint, ssh.FingerprintSHA256(k.PublicKey())) + }) +} + func TestMaybeFile(t *testing.T) { t.Run("is a file", func(t *testing.T) { is := is.New(t)