├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd ├── combine.go ├── root.go ├── root_test.go └── split.go ├── doc ├── assets │ ├── diagram.excalidraw │ ├── diagram.png │ └── interactive-usage.gif └── background.md ├── go.mod ├── go.sum ├── justfile ├── main.go ├── shamir ├── combine.go └── split.go └── utils ├── bufio.go ├── encoding.go └── os.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-go@v1 15 | with: 16 | go-version: 1.20 17 | - uses: extractions/setup-just@v1 18 | with: 19 | just-version: 1.14.0 20 | - run: just install-dependencies 21 | - run: just test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | dist/ 17 | 18 | # Environment variables 19 | .env 20 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | - go generate ./... 5 | 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | 14 | archives: 15 | - replacements: 16 | darwin: Darwin 17 | linux: Linux 18 | windows: Windows 19 | 386: i386 20 | amd64: x86_64 21 | 22 | checksum: 23 | name_template: 'checksums.txt' 24 | 25 | snapshot: 26 | name_template: '{{ .Tag }}' 27 | 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | 35 | brews: 36 | - tap: 37 | owner: incipher 38 | name: homebrew-tap 39 | homepage: https://incipher.io/shamir 40 | description: "Split and combine secrets using Shamir's Secret Sharing algorithm." 41 | license: CC0 42 | url_template: 'https://github.com/incipher/shamir/releases/download/{{ .Tag }}/{{ .ArtifactName }}' 43 | test: | 44 | system "#{bin}/shamir --version" 45 | skip_upload: true 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Moved to [Codeberg](https://codeberg.org/abdelrahman/shamir). 3 | 4 |
5 | 6 | 7 |

shamir

8 | 9 |

Split and combine secrets using Shamir's Secret Sharing algorithm

10 | 11 | 12 | 13 | 14 |
15 | 16 | ## Table of Contents 17 | 18 | - [Table of Contents](#table-of-contents) 19 | - [Description](#description) 20 | - [Background](#background) 21 | - [Installation](#installation) 22 | - [Usage](#usage) 23 | - [Interactive](#interactive) 24 | - [Non-interactive](#non-interactive) 25 | - [License](#license) 26 | 27 | ## Description 28 | 29 | Featuring UNIX-style composability, this command-line tool facilitates splitting and combining secrets using [HashiCorp Vault's implementation](https://github.com/hashicorp/vault/blob/main/shamir/shamir.go) of [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) algorithm. 30 | 31 | ## Background 32 | 33 | [What is Shamir's Secret Sharing algorithm?](./doc/background.md) 34 | 35 | ## Installation 36 | 37 | | Platform | Package manager | Command | 38 | | --------------------- | ------------------------------------------------------- | ------------------------------------ | 39 | | Linux, macOS | [Homebrew](https://brew.sh) | `$ brew install incipher/tap/shamir` | 40 | | Linux, macOS, Windows | [Binaries](https://github.com/incipher/shamir/releases) | | 41 | 42 | ## Usage 43 | 44 | ### Interactive 45 | 46 | ![A GIF showing how to use shamir interactively](./doc/assets/interactive-usage.gif) 47 | 48 | ### Non-interactive 49 | 50 | ``` 51 | $ echo "SayHelloToMyLittleFriend" | shamir split -n 5 -k 3 > shares.txt 52 | Secret: ************************ 53 | ``` 54 | 55 | ``` 56 | $ head -n 3 shares.txt | shamir combine -k 3 57 | SayHelloToMyLittleFriend 58 | ``` 59 | 60 | ## License 61 | 62 | CC0 63 | -------------------------------------------------------------------------------- /cmd/combine.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/manifoldco/promptui" 8 | "github.com/spf13/cobra" 9 | 10 | "incipher.io/shamir/shamir" 11 | "incipher.io/shamir/utils" 12 | ) 13 | 14 | // Generates the combine command. 15 | func generateCombineCommand( 16 | inputSource io.Reader, 17 | outputDestination io.Writer, 18 | errorDestination io.Writer, 19 | isTerminal bool, 20 | ) *cobra.Command { 21 | var thresholdCount int 22 | 23 | combineCommand := &cobra.Command{ 24 | Use: "combine", 25 | Short: "Reconstruct a secret from shares", 26 | Long: "Reconstructs a secret from shares.", 27 | Args: cobra.NoArgs, 28 | Run: runCombineCommand( 29 | inputSource, 30 | outputDestination, 31 | errorDestination, 32 | isTerminal, 33 | &thresholdCount, 34 | ), 35 | } 36 | 37 | combineCommand.Flags().IntVarP( 38 | &thresholdCount, 39 | "threshold", 40 | "k", 41 | 0, 42 | "number of shares necessary to reconstruct the secret", 43 | ) 44 | 45 | combineCommand.MarkFlagRequired("threshold") 46 | 47 | return combineCommand 48 | } 49 | 50 | // Runs the combine command. 51 | func runCombineCommand( 52 | inputSource io.Reader, 53 | outputDestination io.Writer, 54 | errorDestination io.Writer, 55 | isTerminal bool, 56 | thresholdCount *int, 57 | ) func(cmd *cobra.Command, args []string) { 58 | return func(cmd *cobra.Command, args []string) { 59 | if *thresholdCount < 2 || *thresholdCount > 255 { 60 | utils.ExitWithError(errorDestination, fmt.Errorf("threshold must be between 2 and 255")) 61 | } 62 | 63 | var shares []string 64 | var err error 65 | 66 | if isTerminal { 67 | shares, err = readSharesFromPrompts( 68 | inputSource, 69 | outputDestination, 70 | errorDestination, 71 | isTerminal, 72 | thresholdCount, 73 | ) 74 | } else { 75 | shares, err = readSharesFromInputSource( 76 | inputSource, 77 | outputDestination, 78 | errorDestination, 79 | isTerminal, 80 | thresholdCount, 81 | ) 82 | } 83 | if err != nil { 84 | utils.ExitWithError(errorDestination, err) 85 | } 86 | 87 | secret, err := shamir.Combine(shares) 88 | if err != nil { 89 | utils.ExitWithError(errorDestination, err) 90 | } 91 | 92 | _, err = fmt.Fprintln(outputDestination, secret) 93 | if err != nil { 94 | utils.ExitWithError(errorDestination, err) 95 | } 96 | } 97 | } 98 | 99 | func readSharesFromPrompts( 100 | inputSource io.Reader, 101 | outputDestination io.Writer, 102 | errorDestination io.Writer, 103 | isTerminal bool, 104 | thresholdCount *int, 105 | ) ([]string, error) { 106 | shares := make([]string, *thresholdCount) 107 | 108 | for i := 0; i < *thresholdCount; i++ { 109 | prompt := promptui.Prompt{ 110 | Stdin: utils.NopReadCloser(inputSource), 111 | Stdout: utils.NopWriteCloser(errorDestination), 112 | Label: fmt.Sprintf("Share #%d", i+1), 113 | Validate: func(input string) error { 114 | if len(input) == 0 { 115 | return fmt.Errorf("share must not be empty") 116 | } 117 | 118 | if len(input)%2 != 0 { 119 | return fmt.Errorf("share must not be of odd length") 120 | } 121 | 122 | if _, err := utils.HexToByteArray(input); err != nil { 123 | return fmt.Errorf("share must be in hexadecimal encoding") 124 | } 125 | 126 | return nil 127 | }, 128 | } 129 | 130 | share, err := prompt.Run() 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | shares[i] = share 136 | } 137 | 138 | return shares, nil 139 | } 140 | 141 | func readSharesFromInputSource( 142 | inputSource io.Reader, 143 | outputDestination io.Writer, 144 | errorDestination io.Writer, 145 | isTerminal bool, 146 | thresholdCount *int, 147 | ) ([]string, error) { 148 | shares := utils.ReadLines(inputSource) 149 | 150 | if len(shares) != *thresholdCount { 151 | return nil, fmt.Errorf("number of shares must be equal to threshold") 152 | } 153 | 154 | for i, share := range shares { 155 | if len(share) == 0 { 156 | return nil, fmt.Errorf("share #%d must not be empty", i+1) 157 | } 158 | 159 | if len(share)%2 != 0 { 160 | return nil, fmt.Errorf("share #%d must not be of odd length", i+1) 161 | } 162 | 163 | if _, err := utils.HexToByteArray(share); err != nil { 164 | return nil, fmt.Errorf("share #%d must be in hexadecimal encoding", i+1) 165 | } 166 | } 167 | 168 | return shares, nil 169 | } 170 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // Generates the root command. 11 | func GenerateRootCommand( 12 | inputSource io.Reader, 13 | outputDestination io.Writer, 14 | errorDestination io.Writer, 15 | isTerminal bool, 16 | ) *cobra.Command { 17 | examples := []string{" $ shamir split -n 5 -k 3", " $ shamir combine -k 3"} 18 | 19 | rootCommand := &cobra.Command{ 20 | Use: "shamir", 21 | Short: "Split and combine secrets using Shamir's Secret Sharing algorithm.", 22 | Long: "Split and combine secrets using Shamir's Secret Sharing algorithm.", 23 | Version: "0.6.0", 24 | Example: strings.Join(examples, "\n"), 25 | } 26 | 27 | rootCommand.SetIn(inputSource) 28 | rootCommand.SetOut(outputDestination) 29 | rootCommand.SetErr(errorDestination) 30 | 31 | rootCommand.AddCommand( 32 | generateSplitCommand( 33 | inputSource, 34 | outputDestination, 35 | errorDestination, 36 | isTerminal, 37 | ), 38 | ) 39 | rootCommand.AddCommand( 40 | generateCombineCommand( 41 | inputSource, 42 | outputDestination, 43 | errorDestination, 44 | isTerminal, 45 | ), 46 | ) 47 | 48 | return rootCommand 49 | } 50 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/matryer/is" 10 | "incipher.io/shamir/utils" 11 | ) 12 | 13 | const ( 14 | isTerminal = false 15 | ) 16 | 17 | func TestRootCommand(t *testing.T) { 18 | is := is.New(t) 19 | 20 | inputReader, _, _ := createBufferedReaderAndWriter() 21 | outputReader, outputWriter, _ := createBufferedReaderAndWriter() 22 | 23 | rootCommand := GenerateRootCommand(inputReader, outputWriter, outputWriter, isTerminal) 24 | err := rootCommand.Execute() 25 | is.NoErr(err) 26 | 27 | err = outputWriter.Flush() 28 | is.NoErr(err) 29 | 30 | outputLines := utils.ReadLines(outputReader) 31 | 32 | is.Equal( 33 | outputLines[0], 34 | "Split and combine secrets using Shamir's Secret Sharing algorithm.", 35 | ) 36 | } 37 | 38 | func TestSplitCommand(t *testing.T) { 39 | is := is.New(t) 40 | 41 | secret := "SayHelloToMyLittleFriend\n" 42 | sharesCount := 3 43 | thresholdCount := 2 44 | 45 | inputReader, _, inputBuffer := createBufferedReaderAndWriter() 46 | outputReader, outputWriter, _ := createBufferedReaderAndWriter() 47 | _, errorWriter, _ := createBufferedReaderAndWriter() 48 | 49 | _, err := inputBuffer.WriteString(secret) 50 | is.NoErr(err) 51 | 52 | rootCommand := GenerateRootCommand(inputReader, outputWriter, errorWriter, isTerminal) 53 | rootCommand.SetArgs( 54 | []string{ 55 | "split", 56 | "-n", 57 | fmt.Sprint(sharesCount), 58 | "-k", 59 | fmt.Sprint(thresholdCount), 60 | }, 61 | ) 62 | 63 | err = rootCommand.Execute() 64 | is.NoErr(err) 65 | 66 | err = outputWriter.Flush() 67 | is.NoErr(err) 68 | 69 | shares := utils.ReadLines(outputReader) 70 | is.Equal(len(shares), sharesCount) 71 | 72 | shareLength := len(shares[0]) 73 | is.True(shareLength > 0) 74 | 75 | for _, share := range shares { 76 | is.Equal(len(share), shareLength) 77 | } 78 | } 79 | 80 | func TestCombineCommand(t *testing.T) { 81 | is := is.New(t) 82 | 83 | shares := []string{ 84 | "67442ef838a57cbc3063a487d7ca861cf490b9026f5f3a41be\n", 85 | "9ef082cd4f3456dc4bf161460a7cd5f580ed1fd426fa3ff5d7\n", 86 | } 87 | thresholdCount := len(shares) 88 | 89 | inputReader, _, inputBuffer := createBufferedReaderAndWriter() 90 | outputReader, outputWriter, _ := createBufferedReaderAndWriter() 91 | _, errorWriter, _ := createBufferedReaderAndWriter() 92 | 93 | for _, share := range shares { 94 | _, err := inputBuffer.WriteString(share) 95 | is.NoErr(err) 96 | } 97 | 98 | rootCommand := GenerateRootCommand(inputReader, outputWriter, errorWriter, isTerminal) 99 | rootCommand.SetArgs( 100 | []string{ 101 | "combine", 102 | "-k", 103 | fmt.Sprint(thresholdCount), 104 | }, 105 | ) 106 | 107 | err := rootCommand.Execute() 108 | is.NoErr(err) 109 | 110 | err = outputWriter.Flush() 111 | is.NoErr(err) 112 | 113 | outputLines := utils.ReadLines(outputReader) 114 | is.Equal(len(outputLines), 1) 115 | 116 | secret := outputLines[0] 117 | is.Equal(secret, "SayHelloToMyLittleFriend") 118 | } 119 | 120 | func createBufferedReaderAndWriter() (*bufio.Reader, *bufio.Writer, *bytes.Buffer) { 121 | buffer := bytes.NewBuffer(nil) 122 | reader := bufio.NewReader(buffer) 123 | writer := bufio.NewWriter(buffer) 124 | 125 | return reader, writer, buffer 126 | } 127 | -------------------------------------------------------------------------------- /cmd/split.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/manifoldco/promptui" 9 | "github.com/spf13/cobra" 10 | 11 | "incipher.io/shamir/shamir" 12 | "incipher.io/shamir/utils" 13 | ) 14 | 15 | // Generates the split command. 16 | func generateSplitCommand( 17 | inputSource io.Reader, 18 | outputDestination io.Writer, 19 | errorDestination io.Writer, 20 | isTerminal bool, 21 | ) *cobra.Command { 22 | var sharesCount int 23 | var thresholdCount int 24 | 25 | splitCommand := &cobra.Command{ 26 | Use: "split", 27 | Short: "Split a secret into shares", 28 | Long: `Splits a secret into shares (of length n), which a subset 29 | thereof (of length k) is necessary to reconstruct the 30 | original secret.`, 31 | Args: cobra.NoArgs, 32 | Run: runSplitCommand( 33 | inputSource, 34 | outputDestination, 35 | errorDestination, 36 | isTerminal, 37 | &sharesCount, 38 | &thresholdCount, 39 | ), 40 | } 41 | 42 | splitCommand.Flags().IntVarP( 43 | &sharesCount, 44 | "shares", 45 | "n", 46 | 0, 47 | "number of shares to be generated", 48 | ) 49 | 50 | splitCommand.Flags().IntVarP( 51 | &thresholdCount, 52 | "threshold", 53 | "k", 54 | 0, 55 | "number of shares necessary to reconstruct the secret", 56 | ) 57 | 58 | splitCommand.MarkFlagRequired("shares") 59 | splitCommand.MarkFlagRequired("threshold") 60 | 61 | return splitCommand 62 | } 63 | 64 | // Runs the split command. 65 | func runSplitCommand( 66 | inputSource io.Reader, 67 | outputDestination io.Writer, 68 | errorDestination io.Writer, 69 | isTerminal bool, 70 | sharesCount *int, 71 | thresholdCount *int, 72 | ) func(cmd *cobra.Command, args []string) { 73 | return func(cmd *cobra.Command, args []string) { 74 | secret, err := readSecretFromPrompt( 75 | inputSource, 76 | outputDestination, 77 | errorDestination, 78 | isTerminal, 79 | sharesCount, 80 | thresholdCount, 81 | ) 82 | if err != nil { 83 | utils.ExitWithError(errorDestination, err) 84 | } 85 | 86 | shares, err := shamir.Split( 87 | secret, 88 | *sharesCount, 89 | *thresholdCount, 90 | ) 91 | if err != nil { 92 | utils.ExitWithError(errorDestination, err) 93 | } 94 | 95 | sharesConcatenated := strings.Join(shares, "\n") 96 | _, err = fmt.Fprintln(outputDestination, sharesConcatenated) 97 | if err != nil { 98 | utils.ExitWithError(errorDestination, err) 99 | } 100 | } 101 | } 102 | 103 | func readSecretFromPrompt( 104 | inputSource io.Reader, 105 | outputDestination io.Writer, 106 | errorDestination io.Writer, 107 | isTerminal bool, 108 | sharesCount *int, 109 | thresholdCount *int, 110 | ) (string, error) { 111 | prompt := promptui.Prompt{ 112 | Stdin: utils.NopReadCloser(inputSource), 113 | Stdout: utils.NopWriteCloser(errorDestination), 114 | Label: "Secret", 115 | Mask: '*', 116 | Validate: func(input string) error { 117 | if len(input) == 0 { 118 | return fmt.Errorf("secret must not be empty") 119 | } 120 | 121 | return nil 122 | }, 123 | } 124 | 125 | return prompt.Run() 126 | } 127 | -------------------------------------------------------------------------------- /doc/assets/diagram.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://canvas.abdelrahman.sh", 5 | "elements": [ 6 | { 7 | "id": "8p_o2Ao7yN2h-_gdTI_CA", 8 | "type": "rectangle", 9 | "x": 445.1590805053711, 10 | "y": 384.1558619907924, 11 | "width": 340.0000305175782, 12 | "height": 57.27267456054687, 13 | "angle": 0, 14 | "strokeColor": "#000000", 15 | "backgroundColor": "transparent", 16 | "fillStyle": "hachure", 17 | "strokeWidth": 1, 18 | "strokeStyle": "solid", 19 | "roughness": 2, 20 | "opacity": 100, 21 | "groupIds": [ 22 | "-jNYNHpYwUqPnV7stKdG8" 23 | ], 24 | "strokeSharpness": "round", 25 | "seed": 722790352, 26 | "version": 1004, 27 | "versionNonce": 1079737136, 28 | "isDeleted": false, 29 | "boundElementIds": [ 30 | "F8Tfz-L1JERGLHiQlD0ke", 31 | "c9IClyhAJVfiPPXPWkxR-", 32 | "KTdVoDaaeyGxIs4OPN8rw", 33 | "yi74TRNEXPrlmR32MPXxw", 34 | "_Ta6podNLl_5jMpGGOeA3", 35 | "sbz987DTumvGL1NCU8TCq" 36 | ] 37 | }, 38 | { 39 | "id": "kjbohOUjVFGjqdiYgpj2A", 40 | "type": "text", 41 | "x": 474.65909576416016, 42 | "y": 400.79219927106584, 43 | "width": 281, 44 | "height": 24, 45 | "angle": 0, 46 | "strokeColor": "#000000", 47 | "backgroundColor": "transparent", 48 | "fillStyle": "hachure", 49 | "strokeWidth": 1, 50 | "strokeStyle": "solid", 51 | "roughness": 2, 52 | "opacity": 100, 53 | "groupIds": [ 54 | "-jNYNHpYwUqPnV7stKdG8" 55 | ], 56 | "strokeSharpness": "round", 57 | "seed": 1036479952, 58 | "version": 837, 59 | "versionNonce": 563154896, 60 | "isDeleted": false, 61 | "boundElementIds": null, 62 | "text": "SayHelloToMyLittleFriend", 63 | "fontSize": 20, 64 | "fontFamily": 3, 65 | "textAlign": "center", 66 | "verticalAlign": "middle", 67 | "baseline": 19 68 | }, 69 | { 70 | "type": "rectangle", 71 | "version": 691, 72 | "versionNonce": 1750920144, 73 | "isDeleted": false, 74 | "id": "vmJjNGkW12jS7DPoS9orz", 75 | "fillStyle": "hachure", 76 | "strokeWidth": 1, 77 | "strokeStyle": "solid", 78 | "roughness": 2, 79 | "opacity": 100, 80 | "angle": 0, 81 | "x": 944.7272033691406, 82 | "y": 230.06063334147134, 83 | "strokeColor": "#000000", 84 | "backgroundColor": "transparent", 85 | "width": 642.727264404297, 86 | "height": 56.363616943359375, 87 | "seed": 607728592, 88 | "groupIds": [ 89 | "1omzrz4xlPcGVVkxFymsw" 90 | ], 91 | "strokeSharpness": "round", 92 | "boundElementIds": [ 93 | "c9IClyhAJVfiPPXPWkxR-", 94 | "Z9rxxJFsADAvsjbBd6QGQ", 95 | "_6tfMFXwt36YKF9R1EJh7" 96 | ] 97 | }, 98 | { 99 | "type": "text", 100 | "version": 510, 101 | "versionNonce": 1271655728, 102 | "isDeleted": false, 103 | "id": "zLUqV-YHtVQCH8ucxc1b9", 104 | "fillStyle": "hachure", 105 | "strokeWidth": 1, 106 | "strokeStyle": "solid", 107 | "roughness": 2, 108 | "opacity": 100, 109 | "angle": 0, 110 | "x": 973.0908355712891, 111 | "y": 246.24244181315103, 112 | "strokeColor": "#000000", 113 | "backgroundColor": "transparent", 114 | "width": 586, 115 | "height": 24, 116 | "seed": 612514256, 117 | "groupIds": [ 118 | "1omzrz4xlPcGVVkxFymsw" 119 | ], 120 | "strokeSharpness": "round", 121 | "boundElementIds": [], 122 | "fontSize": 20, 123 | "fontFamily": 3, 124 | "text": "b55fcb1a94ea9036976b6715557df1f7a9594aa28c0c8ddc76", 125 | "baseline": 19, 126 | "textAlign": "center", 127 | "verticalAlign": "top" 128 | }, 129 | { 130 | "type": "rectangle", 131 | "version": 766, 132 | "versionNonce": 2059038512, 133 | "isDeleted": false, 134 | "id": "C0aoFVocdX1nSAFYlVkBx", 135 | "fillStyle": "hachure", 136 | "strokeWidth": 1, 137 | "strokeStyle": "solid", 138 | "roughness": 2, 139 | "opacity": 100, 140 | "angle": 0, 141 | "x": 944.7272033691406, 142 | "y": 307.5000305175781, 143 | "strokeColor": "#000000", 144 | "backgroundColor": "transparent", 145 | "width": 642.727264404297, 146 | "height": 56.363616943359375, 147 | "seed": 28999632, 148 | "groupIds": [ 149 | "B9_bz-TkjooygitbEuLxL" 150 | ], 151 | "strokeSharpness": "round", 152 | "boundElementIds": [ 153 | "KTdVoDaaeyGxIs4OPN8rw" 154 | ] 155 | }, 156 | { 157 | "type": "text", 158 | "version": 624, 159 | "versionNonce": 2009816016, 160 | "isDeleted": false, 161 | "id": "451FpentntI2z4pr9DHvY", 162 | "fillStyle": "hachure", 163 | "strokeWidth": 1, 164 | "strokeStyle": "solid", 165 | "roughness": 2, 166 | "opacity": 100, 167 | "angle": 0, 168 | "x": 973.0908355712891, 169 | "y": 323.6818389892578, 170 | "strokeColor": "#000000", 171 | "backgroundColor": "transparent", 172 | "width": 586, 173 | "height": 24, 174 | "seed": 1543112144, 175 | "groupIds": [ 176 | "B9_bz-TkjooygitbEuLxL" 177 | ], 178 | "strokeSharpness": "round", 179 | "boundElementIds": [], 180 | "fontSize": 20, 181 | "fontFamily": 3, 182 | "text": "c7b340426663c59cca8f279d556846374b6180a893237e3188", 183 | "baseline": 19, 184 | "textAlign": "center", 185 | "verticalAlign": "middle" 186 | }, 187 | { 188 | "type": "rectangle", 189 | "version": 734, 190 | "versionNonce": 1107626288, 191 | "isDeleted": false, 192 | "id": "8bxYdUsXOqIVMVDwEF57X", 193 | "fillStyle": "hachure", 194 | "strokeWidth": 1, 195 | "strokeStyle": "solid", 196 | "roughness": 2, 197 | "opacity": 100, 198 | "angle": 0, 199 | "x": 944.7272033691406, 200 | "y": 386.45457458496094, 201 | "strokeColor": "#000000", 202 | "backgroundColor": "transparent", 203 | "width": 642.727264404297, 204 | "height": 56.363616943359375, 205 | "seed": 274254800, 206 | "groupIds": [ 207 | "9Gpt37CuTz23OTU2cteX6" 208 | ], 209 | "strokeSharpness": "round", 210 | "boundElementIds": [ 211 | "yi74TRNEXPrlmR32MPXxw", 212 | "Wr3-6xcTcWcPCQV5VCVmz", 213 | "wRMA-3rXy-gy5o2-MNSPQ" 214 | ] 215 | }, 216 | { 217 | "type": "text", 218 | "version": 585, 219 | "versionNonce": 328163792, 220 | "isDeleted": false, 221 | "id": "CiEqDT6PQxNXRdov5Sxch", 222 | "fillStyle": "hachure", 223 | "strokeWidth": 1, 224 | "strokeStyle": "solid", 225 | "roughness": 2, 226 | "opacity": 100, 227 | "angle": 0, 228 | "x": 973.0908355712891, 229 | "y": 402.6363830566406, 230 | "strokeColor": "#000000", 231 | "backgroundColor": "transparent", 232 | "width": 586, 233 | "height": 24, 234 | "seed": 418304464, 235 | "groupIds": [ 236 | "9Gpt37CuTz23OTU2cteX6" 237 | ], 238 | "strokeSharpness": "round", 239 | "boundElementIds": [], 240 | "fontSize": 20, 241 | "fontFamily": 3, 242 | "text": "be5beb08533a4a3611c46e9d971127ff59b77873995df6610b", 243 | "baseline": 19, 244 | "textAlign": "center", 245 | "verticalAlign": "top" 246 | }, 247 | { 248 | "type": "rectangle", 249 | "version": 802, 250 | "versionNonce": 955015984, 251 | "isDeleted": false, 252 | "id": "fNIlXl9sMnoVKBqsvRU76", 253 | "fillStyle": "hachure", 254 | "strokeWidth": 1, 255 | "strokeStyle": "solid", 256 | "roughness": 2, 257 | "opacity": 100, 258 | "angle": 0, 259 | "x": 944.7272033691406, 260 | "y": 465.40911865234375, 261 | "strokeColor": "#000000", 262 | "backgroundColor": "transparent", 263 | "width": 642.727264404297, 264 | "height": 56.363616943359375, 265 | "seed": 1825839056, 266 | "groupIds": [ 267 | "Do0fLH__IqO4hYvn94L2l" 268 | ], 269 | "strokeSharpness": "round", 270 | "boundElementIds": [ 271 | "_Ta6podNLl_5jMpGGOeA3" 272 | ] 273 | }, 274 | { 275 | "type": "text", 276 | "version": 642, 277 | "versionNonce": 1692575696, 278 | "isDeleted": false, 279 | "id": "4FL_fg-JekFay8u1_vcJs", 280 | "fillStyle": "hachure", 281 | "strokeWidth": 1, 282 | "strokeStyle": "solid", 283 | "roughness": 2, 284 | "opacity": 100, 285 | "angle": 0, 286 | "x": 972.1817779541016, 287 | "y": 481.59092712402344, 288 | "strokeColor": "#000000", 289 | "backgroundColor": "transparent", 290 | "width": 586, 291 | "height": 24, 292 | "seed": 1612750288, 293 | "groupIds": [ 294 | "Do0fLH__IqO4hYvn94L2l" 295 | ], 296 | "strokeSharpness": "round", 297 | "boundElementIds": [], 298 | "fontSize": 20, 299 | "fontFamily": 3, 300 | "text": "932711479caee6016bdac5fafe00ef52911a60b48762356ae6", 301 | "baseline": 19, 302 | "textAlign": "center", 303 | "verticalAlign": "top" 304 | }, 305 | { 306 | "type": "rectangle", 307 | "version": 853, 308 | "versionNonce": 1306033456, 309 | "isDeleted": false, 310 | "id": "-NSBNxLmVHLi80D5gfOj6", 311 | "fillStyle": "hachure", 312 | "strokeWidth": 1, 313 | "strokeStyle": "solid", 314 | "roughness": 2, 315 | "opacity": 100, 316 | "angle": 0, 317 | "x": 944.7272033691406, 318 | "y": 544.3636627197266, 319 | "strokeColor": "#000000", 320 | "backgroundColor": "transparent", 321 | "width": 642.727264404297, 322 | "height": 56.363616943359375, 323 | "seed": 65823696, 324 | "groupIds": [ 325 | "I9B64W2_UfZNrrcmdHXS3" 326 | ], 327 | "strokeSharpness": "round", 328 | "boundElementIds": [ 329 | "sbz987DTumvGL1NCU8TCq", 330 | "qnGHe8HzBw-LhpXbGCyvX" 331 | ] 332 | }, 333 | { 334 | "type": "text", 335 | "version": 693, 336 | "versionNonce": 2010983888, 337 | "isDeleted": false, 338 | "id": "HPnpU7eBQD4R7snsG6UnT", 339 | "fillStyle": "hachure", 340 | "strokeWidth": 1, 341 | "strokeStyle": "solid", 342 | "roughness": 2, 343 | "opacity": 100, 344 | "angle": 0, 345 | "x": 973.0908355712891, 346 | "y": 560.5454711914062, 347 | "strokeColor": "#000000", 348 | "backgroundColor": "transparent", 349 | "width": 586, 350 | "height": 24, 351 | "seed": 105867728, 352 | "groupIds": [ 353 | "I9B64W2_UfZNrrcmdHXS3" 354 | ], 355 | "strokeSharpness": "round", 356 | "boundElementIds": [], 357 | "fontSize": 20, 358 | "fontFamily": 3, 359 | "text": "01bd0f50b71c9fa3b3e6098ebaad87d52372cfe5b91b1762d8", 360 | "baseline": 19, 361 | "textAlign": "center", 362 | "verticalAlign": "top" 363 | }, 364 | { 365 | "id": "c9IClyhAJVfiPPXPWkxR-", 366 | "type": "arrow", 367 | "x": 794.7490164637566, 368 | "y": 393.7868453778476, 369 | "width": 137.272705078125, 370 | "height": 138.77112498855305, 371 | "angle": 0, 372 | "strokeColor": "#000000", 373 | "backgroundColor": "transparent", 374 | "fillStyle": "hachure", 375 | "strokeWidth": 1, 376 | "strokeStyle": "solid", 377 | "roughness": 1, 378 | "opacity": 100, 379 | "groupIds": [], 380 | "strokeSharpness": "round", 381 | "seed": 2083762992, 382 | "version": 118, 383 | "versionNonce": 460255696, 384 | "isDeleted": false, 385 | "boundElementIds": null, 386 | "points": [ 387 | [ 388 | 0, 389 | 0 390 | ], 391 | [ 392 | 137.272705078125, 393 | -138.77112498855305 394 | ] 395 | ], 396 | "lastCommittedPoint": null, 397 | "startBinding": { 398 | "elementId": "8p_o2Ao7yN2h-_gdTI_CA", 399 | "focus": 0.8126749779871376, 400 | "gap": 9.589905440807343 401 | }, 402 | "endBinding": { 403 | "elementId": "vmJjNGkW12jS7DPoS9orz", 404 | "focus": 0.9656966366568633, 405 | "gap": 12.705481827259064 406 | }, 407 | "startArrowhead": null, 408 | "endArrowhead": "arrow" 409 | }, 410 | { 411 | "id": "KTdVoDaaeyGxIs4OPN8rw", 412 | "type": "arrow", 413 | "x": 794.7490164637566, 414 | "y": 406.338692682328, 415 | "width": 137.272705078125, 416 | "height": 71.12459373138051, 417 | "angle": 0, 418 | "strokeColor": "#000000", 419 | "backgroundColor": "transparent", 420 | "fillStyle": "hachure", 421 | "strokeWidth": 1, 422 | "strokeStyle": "solid", 423 | "roughness": 1, 424 | "opacity": 100, 425 | "groupIds": [], 426 | "strokeSharpness": "round", 427 | "seed": 899243824, 428 | "version": 107, 429 | "versionNonce": 1116002608, 430 | "isDeleted": false, 431 | "boundElementIds": null, 432 | "points": [ 433 | [ 434 | 0, 435 | 0 436 | ], 437 | [ 438 | 137.272705078125, 439 | -71.12459373138051 440 | ] 441 | ], 442 | "lastCommittedPoint": null, 443 | "startBinding": { 444 | "elementId": "8p_o2Ao7yN2h-_gdTI_CA", 445 | "focus": 0.7411283008217929, 446 | "gap": 9.589905440807343 447 | }, 448 | "endBinding": { 449 | "elementId": "C0aoFVocdX1nSAFYlVkBx", 450 | "focus": 0.8914625267024088, 451 | "gap": 12.705481827259064 452 | }, 453 | "startArrowhead": null, 454 | "endArrowhead": "arrow" 455 | }, 456 | { 457 | "id": "yi74TRNEXPrlmR32MPXxw", 458 | "type": "arrow", 459 | "x": 795.6581351161004, 460 | "y": 416.6537515207418, 461 | "width": 136.36358642578114, 462 | "height": 1.623897633556794, 463 | "angle": 0, 464 | "strokeColor": "#000000", 465 | "backgroundColor": "transparent", 466 | "fillStyle": "hachure", 467 | "strokeWidth": 1, 468 | "strokeStyle": "solid", 469 | "roughness": 1, 470 | "opacity": 100, 471 | "groupIds": [], 472 | "strokeSharpness": "round", 473 | "seed": 1858431280, 474 | "version": 78, 475 | "versionNonce": 934580688, 476 | "isDeleted": false, 477 | "boundElementIds": null, 478 | "points": [ 479 | [ 480 | 0, 481 | 0 482 | ], 483 | [ 484 | 136.36358642578114, 485 | -1.623897633556794 486 | ] 487 | ], 488 | "lastCommittedPoint": null, 489 | "startBinding": { 490 | "elementId": "8p_o2Ao7yN2h-_gdTI_CA", 491 | "focus": 0.18083863109662682, 492 | "gap": 10.499024093151093 493 | }, 494 | "endBinding": { 495 | "elementId": "8bxYdUsXOqIVMVDwEF57X", 496 | "focus": 0.11199485181008462, 497 | "gap": 12.705481827259064 498 | }, 499 | "startArrowhead": null, 500 | "endArrowhead": "arrow" 501 | }, 502 | { 503 | "id": "_Ta6podNLl_5jMpGGOeA3", 504 | "type": "arrow", 505 | "x": 796.5671927332878, 506 | "y": 424.56125166249797, 507 | "width": 138.18182373046875, 508 | "height": 69.75454001054021, 509 | "angle": 0, 510 | "strokeColor": "#000000", 511 | "backgroundColor": "transparent", 512 | "fillStyle": "hachure", 513 | "strokeWidth": 1, 514 | "strokeStyle": "solid", 515 | "roughness": 1, 516 | "opacity": 100, 517 | "groupIds": [], 518 | "strokeSharpness": "round", 519 | "seed": 1185754576, 520 | "version": 96, 521 | "versionNonce": 955396912, 522 | "isDeleted": false, 523 | "boundElementIds": null, 524 | "points": [ 525 | [ 526 | 0, 527 | 0 528 | ], 529 | [ 530 | 138.18182373046875, 531 | 69.75454001054021 532 | ] 533 | ], 534 | "lastCommittedPoint": null, 535 | "startBinding": { 536 | "elementId": "8p_o2Ao7yN2h-_gdTI_CA", 537 | "focus": -0.6981957783443743, 538 | "gap": 11.408081710338593 539 | }, 540 | "endBinding": { 541 | "elementId": "fNIlXl9sMnoVKBqsvRU76", 542 | "focus": -0.8822525961815563, 543 | "gap": 9.978186905384064 544 | }, 545 | "startArrowhead": null, 546 | "endArrowhead": "arrow" 547 | }, 548 | { 549 | "id": "sbz987DTumvGL1NCU8TCq", 550 | "type": "arrow", 551 | "x": 795.6581351161003, 552 | "y": 434.4651946550263, 553 | "width": 140.9090576171875, 554 | "height": 142.67343136685764, 555 | "angle": 0, 556 | "strokeColor": "#000000", 557 | "backgroundColor": "transparent", 558 | "fillStyle": "hachure", 559 | "strokeWidth": 1, 560 | "strokeStyle": "solid", 561 | "roughness": 1, 562 | "opacity": 100, 563 | "groupIds": [], 564 | "strokeSharpness": "round", 565 | "seed": 1539560752, 566 | "version": 98, 567 | "versionNonce": 752998352, 568 | "isDeleted": false, 569 | "boundElementIds": null, 570 | "points": [ 571 | [ 572 | 0, 573 | 0 574 | ], 575 | [ 576 | 140.9090576171875, 577 | 142.67343136685764 578 | ] 579 | ], 580 | "lastCommittedPoint": null, 581 | "startBinding": { 582 | "elementId": "8p_o2Ao7yN2h-_gdTI_CA", 583 | "focus": -0.8025705518500119, 584 | "gap": 10.499024093151093 585 | }, 586 | "endBinding": { 587 | "elementId": "-NSBNxLmVHLi80D5gfOj6", 588 | "focus": -0.9566521576447085, 589 | "gap": 8.160010635852814 590 | }, 591 | "startArrowhead": null, 592 | "endArrowhead": "arrow" 593 | }, 594 | { 595 | "type": "rectangle", 596 | "version": 1272, 597 | "versionNonce": 1822706992, 598 | "isDeleted": false, 599 | "id": "dXBxhiZpRwQOUsNOJPR7v", 600 | "fillStyle": "hachure", 601 | "strokeWidth": 1, 602 | "strokeStyle": "solid", 603 | "roughness": 2, 604 | "opacity": 100, 605 | "angle": 0, 606 | "x": 1746.5059814651813, 607 | "y": 385.0439373850822, 608 | "strokeColor": "#000000", 609 | "backgroundColor": "transparent", 610 | "width": 340.0000305175782, 611 | "height": 57.27267456054687, 612 | "seed": 308391888, 613 | "groupIds": [ 614 | "dpvnVhU3TvFyewF99b-cw" 615 | ], 616 | "strokeSharpness": "round", 617 | "boundElementIds": [ 618 | "F8Tfz-L1JERGLHiQlD0ke", 619 | "c9IClyhAJVfiPPXPWkxR-", 620 | "KTdVoDaaeyGxIs4OPN8rw", 621 | "yi74TRNEXPrlmR32MPXxw", 622 | "_Ta6podNLl_5jMpGGOeA3", 623 | "sbz987DTumvGL1NCU8TCq", 624 | "Z9rxxJFsADAvsjbBd6QGQ", 625 | "_6tfMFXwt36YKF9R1EJh7", 626 | "wRMA-3rXy-gy5o2-MNSPQ", 627 | "qnGHe8HzBw-LhpXbGCyvX" 628 | ] 629 | }, 630 | { 631 | "type": "text", 632 | "version": 1107, 633 | "versionNonce": 1484152272, 634 | "isDeleted": false, 635 | "id": "95i2coc3IPf1uED1rpBY1", 636 | "fillStyle": "hachure", 637 | "strokeWidth": 1, 638 | "strokeStyle": "solid", 639 | "roughness": 2, 640 | "opacity": 100, 641 | "angle": 0, 642 | "x": 1776.0059967239704, 643 | "y": 401.6802746653556, 644 | "strokeColor": "#000000", 645 | "backgroundColor": "transparent", 646 | "width": 281, 647 | "height": 24, 648 | "seed": 809186768, 649 | "groupIds": [ 650 | "dpvnVhU3TvFyewF99b-cw" 651 | ], 652 | "strokeSharpness": "round", 653 | "boundElementIds": [], 654 | "fontSize": 20, 655 | "fontFamily": 3, 656 | "text": "SayHelloToMyLittleFriend", 657 | "baseline": 19, 658 | "textAlign": "center", 659 | "verticalAlign": "middle" 660 | }, 661 | { 662 | "id": "_6tfMFXwt36YKF9R1EJh7", 663 | "type": "arrow", 664 | "x": 1597.816165590099, 665 | "y": 258.1814501020119, 666 | "width": 136.36360168457054, 667 | "height": 142.1754769635903, 668 | "angle": 0, 669 | "strokeColor": "#000000", 670 | "backgroundColor": "transparent", 671 | "fillStyle": "hachure", 672 | "strokeWidth": 1, 673 | "strokeStyle": "solid", 674 | "roughness": 1, 675 | "opacity": 100, 676 | "groupIds": [], 677 | "strokeSharpness": "round", 678 | "seed": 1634935760, 679 | "version": 88, 680 | "versionNonce": 1783642064, 681 | "isDeleted": false, 682 | "boundElementIds": null, 683 | "points": [ 684 | [ 685 | 0, 686 | 0 687 | ], 688 | [ 689 | 136.36360168457054, 690 | 142.1754769635903 691 | ] 692 | ], 693 | "lastCommittedPoint": null, 694 | "startBinding": { 695 | "elementId": "vmJjNGkW12jS7DPoS9orz", 696 | "focus": -0.9521858137630177, 697 | "gap": 10.36169781666149 698 | }, 699 | "endBinding": { 700 | "elementId": "dXBxhiZpRwQOUsNOJPR7v", 701 | "focus": -0.858617330721429, 702 | "gap": 12.326214190512019 703 | }, 704 | "startArrowhead": null, 705 | "endArrowhead": "arrow" 706 | }, 707 | { 708 | "id": "wRMA-3rXy-gy5o2-MNSPQ", 709 | "type": "arrow", 710 | "x": 1596.6798435686146, 711 | "y": 418.2704304144498, 712 | "width": 137.4999237060547, 713 | "height": 2.2727203369140625, 714 | "angle": 0, 715 | "strokeColor": "#000000", 716 | "backgroundColor": "transparent", 717 | "fillStyle": "hachure", 718 | "strokeWidth": 1, 719 | "strokeStyle": "solid", 720 | "roughness": 1, 721 | "opacity": 100, 722 | "groupIds": [], 723 | "strokeSharpness": "round", 724 | "seed": 1644507440, 725 | "version": 146, 726 | "versionNonce": 74891568, 727 | "isDeleted": false, 728 | "boundElementIds": null, 729 | "points": [ 730 | [ 731 | 0, 732 | 0 733 | ], 734 | [ 735 | 137.4999237060547, 736 | -2.2727203369140625 737 | ] 738 | ], 739 | "lastCommittedPoint": null, 740 | "startBinding": { 741 | "elementId": "8bxYdUsXOqIVMVDwEF57X", 742 | "focus": 0.2716434585095164, 743 | "gap": 9.225375795177115 744 | }, 745 | "endBinding": { 746 | "elementId": "dXBxhiZpRwQOUsNOJPR7v", 747 | "focus": 0.02213980348272873, 748 | "gap": 12.326214190512019 749 | }, 750 | "startArrowhead": null, 751 | "endArrowhead": "arrow" 752 | }, 753 | { 754 | "id": "qnGHe8HzBw-LhpXbGCyvX", 755 | "type": "arrow", 756 | "x": 1597.816165590099, 757 | "y": 573.9522694037076, 758 | "width": 142.04551696777344, 759 | "height": 134.09095764160156, 760 | "angle": 0, 761 | "strokeColor": "#000000", 762 | "backgroundColor": "transparent", 763 | "fillStyle": "hachure", 764 | "strokeWidth": 1, 765 | "strokeStyle": "solid", 766 | "roughness": 1, 767 | "opacity": 100, 768 | "groupIds": [], 769 | "strokeSharpness": "round", 770 | "seed": 1117979600, 771 | "version": 117, 772 | "versionNonce": 875899344, 773 | "isDeleted": false, 774 | "boundElementIds": null, 775 | "points": [ 776 | [ 777 | 0, 778 | 0 779 | ], 780 | [ 781 | 142.04551696777344, 782 | -134.09095764160156 783 | ] 784 | ], 785 | "lastCommittedPoint": null, 786 | "startBinding": { 787 | "elementId": "-NSBNxLmVHLi80D5gfOj6", 788 | "focus": 0.9487449350773901, 789 | "gap": 10.36169781666149 790 | }, 791 | "endBinding": { 792 | "elementId": "dXBxhiZpRwQOUsNOJPR7v", 793 | "focus": 0.7433053279154634, 794 | "gap": 6.644298907308894 795 | }, 796 | "startArrowhead": null, 797 | "endArrowhead": "arrow" 798 | }, 799 | { 800 | "type": "rectangle", 801 | "version": 1328, 802 | "versionNonce": 685802448, 803 | "isDeleted": false, 804 | "id": "sfZAXmGv_cYopUkUPn7qG", 805 | "fillStyle": "cross-hatch", 806 | "strokeWidth": 1, 807 | "strokeStyle": "dashed", 808 | "roughness": 2, 809 | "opacity": 100, 810 | "angle": 0, 811 | "x": 1153.818115234375, 812 | "y": 85.20217027432435, 813 | "strokeColor": "#000000", 814 | "backgroundColor": "transparent", 815 | "width": 224.54544067382818, 816 | "height": 57.27267456054687, 817 | "seed": 1296731952, 818 | "groupIds": [ 819 | "Wbr2zX-0jp-S36OlNCL47" 820 | ], 821 | "strokeSharpness": "round", 822 | "boundElementIds": [ 823 | "F8Tfz-L1JERGLHiQlD0ke", 824 | "c9IClyhAJVfiPPXPWkxR-", 825 | "KTdVoDaaeyGxIs4OPN8rw", 826 | "yi74TRNEXPrlmR32MPXxw", 827 | "_Ta6podNLl_5jMpGGOeA3", 828 | "sbz987DTumvGL1NCU8TCq" 829 | ] 830 | }, 831 | { 832 | "type": "text", 833 | "version": 1157, 834 | "versionNonce": 1864649008, 835 | "isDeleted": false, 836 | "id": "aE7B9Zg20rHHrUtpBa3_E", 837 | "fillStyle": "cross-hatch", 838 | "strokeWidth": 1, 839 | "strokeStyle": "dashed", 840 | "roughness": 2, 841 | "opacity": 100, 842 | "angle": 0, 843 | "x": 1200.590835571289, 844 | "y": 96.83850755459778, 845 | "strokeColor": "#000000", 846 | "backgroundColor": "transparent", 847 | "width": 131, 848 | "height": 34, 849 | "seed": 1842166736, 850 | "groupIds": [ 851 | "Wbr2zX-0jp-S36OlNCL47" 852 | ], 853 | "strokeSharpness": "round", 854 | "boundElementIds": [], 855 | "fontSize": 28, 856 | "fontFamily": 3, 857 | "text": "n=5, k=3", 858 | "baseline": 27, 859 | "textAlign": "center", 860 | "verticalAlign": "top" 861 | } 862 | ], 863 | "appState": { 864 | "gridSize": null, 865 | "viewBackgroundColor": "#ffffff" 866 | }, 867 | "files": {} 868 | } -------------------------------------------------------------------------------- /doc/assets/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incipher/shamir/4989a3172ccd15eafbef45c391f95a3f59dd684f/doc/assets/diagram.png -------------------------------------------------------------------------------- /doc/assets/interactive-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incipher/shamir/4989a3172ccd15eafbef45c391f95a3f59dd684f/doc/assets/interactive-usage.gif -------------------------------------------------------------------------------- /doc/background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | Formulated by [Adi Shamir](https://en.wikipedia.org/wiki/Adi_Shamir) (the S in [RSA]()) in his 1979 paper [“How to share a secret”](http://web.mit.edu/6.857/OldStuff/Fall03/ref/Shamir-HowToShareASecret.pdf), Shamir's Secret Sharing is an algorithm that allows you to split a secret (e.g. a [symmetric encryption](https://en.wikipedia.org/wiki/Symmetric-key_algorithm) key) into $n$ shares, which can be combined later to reconstruct that secret. 4 | 5 | ![Diagram](./assets/diagram.png) 6 | 7 | Not all shares need to be present for a successful reconstruction, but actually any subset thereof with a size greater than or equal to the minimum threshold $k$, where $2 \le k \le n$. The algorithm mathematically guarantees that knowledge of $k - 1$ shares reveals absolutely no information about the original secret. 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module incipher.io/shamir 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/hashicorp/vault v1.2.1-0.20230810202620-ba215dbc1259 7 | github.com/manifoldco/promptui v0.9.0 8 | github.com/matryer/is v1.4.0 9 | github.com/spf13/cobra v1.7.0 10 | golang.org/x/term v0.10.0 11 | ) 12 | 13 | require ( 14 | github.com/chzyer/readline v1.5.1 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/spf13/pflag v1.0.5 // indirect 17 | golang.org/x/sys v0.10.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 3 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 8 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 9 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 11 | github.com/hashicorp/vault v1.2.1-0.20230810202620-ba215dbc1259 h1:lrQ1RvwAoKGgD96bJxj7nyA7VEYWFOwZlJXlA7ub6QM= 12 | github.com/hashicorp/vault v1.2.1-0.20230810202620-ba215dbc1259/go.mod h1:Jpl9eYE/ov54db03C2EVMuAmywZVaHqh4rlkfnOash4= 13 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 14 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 15 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 16 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 17 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 18 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 19 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 20 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 21 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 22 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 23 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 24 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 27 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= 29 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | install-dependencies: 4 | go get 5 | 6 | upgrade-dependencies: 7 | go get -u 8 | go mod tidy 9 | 10 | upgrade-go-version version: 11 | go mod edit -go {{version}} 12 | go mod tidy 13 | 14 | test: 15 | go clean -testcache 16 | CGO_ENABLED="0" go test -v ./... 17 | 18 | build: 19 | go build main.go 20 | 21 | publish version: 22 | @echo 'Publishing {{version}} ...' 23 | git tag -a {{version}} -m "{{version}}" -s 24 | git push origin {{version}} 25 | goreleaser release --rm-dist 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | 7 | "golang.org/x/term" 8 | "incipher.io/shamir/cmd" 9 | "incipher.io/shamir/utils" 10 | ) 11 | 12 | func main() { 13 | inputSource := os.Stdin 14 | outputDestination := os.Stdout 15 | errorDestination := os.Stderr 16 | isTerminal := term.IsTerminal(int(syscall.Stdin)) 17 | 18 | rootCommand := cmd.GenerateRootCommand( 19 | inputSource, 20 | outputDestination, 21 | errorDestination, 22 | isTerminal, 23 | ) 24 | 25 | err := rootCommand.Execute() 26 | if err != nil { 27 | utils.ExitWithError(errorDestination, err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /shamir/combine.go: -------------------------------------------------------------------------------- 1 | package shamir 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/vault/shamir" 7 | "incipher.io/shamir/utils" 8 | ) 9 | 10 | // Reconstructs a secret from shares. 11 | func Combine(sharesHex []string) (string, error) { 12 | sharesBytes := make([][]byte, len(sharesHex)) 13 | 14 | for i := range sharesHex { 15 | shareBytes, err := utils.HexToByteArray(sharesHex[i]) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | sharesBytes[i] = shareBytes 21 | } 22 | 23 | if len(sharesBytes) < 2 || len(sharesBytes) > 255 { 24 | return "", fmt.Errorf("shares must be between 2 and 255") 25 | } 26 | 27 | firstShareBytes := sharesBytes[0] 28 | 29 | if len(firstShareBytes) < 2 { 30 | return "", fmt.Errorf("shares must be of length greater than 2 bytes") 31 | } 32 | 33 | for i := 1; i < len(sharesBytes); i++ { 34 | if len(sharesBytes[i]) != len(firstShareBytes) { 35 | return "", fmt.Errorf("shares must be of equal length") 36 | } 37 | } 38 | 39 | secretBytes, err := shamir.Combine(sharesBytes) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | secret := utils.ByteArrayToString(secretBytes) 45 | 46 | return secret, nil 47 | } 48 | -------------------------------------------------------------------------------- /shamir/split.go: -------------------------------------------------------------------------------- 1 | package shamir 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/vault/shamir" 7 | "incipher.io/shamir/utils" 8 | ) 9 | 10 | // Splits a secret into shares (of length sharesCount), 11 | // which a subset thereof (of length thresholdCount) is 12 | // necessary to reconstruct the original secret. 13 | func Split( 14 | secret string, 15 | sharesCount int, 16 | thresholdCount int, 17 | ) ([]string, error) { 18 | if len(secret) == 0 { 19 | return nil, fmt.Errorf("secret must not be empty") 20 | } 21 | 22 | if thresholdCount > sharesCount { 23 | return nil, fmt.Errorf("threshold must be less than or equal shares") 24 | } 25 | 26 | if sharesCount < 2 || sharesCount > 255 { 27 | return nil, fmt.Errorf("shares must be between 2 and 255") 28 | } 29 | 30 | if thresholdCount < 2 || thresholdCount > 255 { 31 | return nil, fmt.Errorf("threshold must be between 2 and 255") 32 | } 33 | 34 | sharesBytes, err := shamir.Split( 35 | utils.StringToByteArray(secret), 36 | sharesCount, 37 | thresholdCount, 38 | ) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | sharesHex := make([]string, len(sharesBytes)) 44 | 45 | for i := range sharesBytes { 46 | sharesHex[i] = utils.ByteArrayToHex(sharesBytes[i]) 47 | } 48 | 49 | return sharesHex, nil 50 | } 51 | -------------------------------------------------------------------------------- /utils/bufio.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // Reads lines from the given Reader. 10 | func ReadLines(reader io.Reader) []string { 11 | scanner := bufio.NewScanner(reader) 12 | scanner.Split(bufio.ScanLines) 13 | 14 | var lines []string 15 | var line string 16 | 17 | for scanner.Scan() { 18 | line = strings.TrimSpace(scanner.Text()) 19 | if line != "" { 20 | lines = append(lines, line) 21 | } 22 | } 23 | 24 | return lines 25 | } 26 | 27 | // Returns an io.ReadCloser with a no-op Close method wrapping the provided reader. 28 | func NopReadCloser(reader io.Reader) io.ReadCloser { 29 | return io.NopCloser(reader) 30 | } 31 | 32 | // Returns an io.WriteCloser with a no-op Close method wrapping the provided writer. 33 | func NopWriteCloser(writer io.Writer) io.WriteCloser { 34 | return &WriteCloser{Writer: writer} 35 | } 36 | 37 | func (writeCloser *WriteCloser) Close() error { 38 | // Noop 39 | return nil 40 | } 41 | 42 | type WriteCloser struct { 43 | io.Writer 44 | } 45 | -------------------------------------------------------------------------------- /utils/encoding.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/hex" 5 | ) 6 | 7 | // Converts a byte array to a string of hex encoding. 8 | func ByteArrayToHex(byteArray []byte) string { 9 | return hex.EncodeToString(byteArray) 10 | } 11 | 12 | // Converts a string of hex encoding to a byte array. 13 | func HexToByteArray(string string) ([]byte, error) { 14 | return hex.DecodeString(string) 15 | } 16 | 17 | // Converts a byte array to a string. 18 | func ByteArrayToString(byteArray []byte) string { 19 | return string(byteArray) 20 | } 21 | 22 | // Converts a string to a byte array. 23 | func StringToByteArray(string string) []byte { 24 | return []byte(string) 25 | } 26 | -------------------------------------------------------------------------------- /utils/os.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // Prints to stderr and exits with an error code. 10 | func ExitWithError(errorDestination io.Writer, err error) { 11 | fmt.Fprintln(errorDestination, err.Error()) 12 | os.Exit(1) 13 | } 14 | --------------------------------------------------------------------------------