├── .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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------