├── .gitignore ├── LICENSE ├── README.md ├── golangci.yml └── src ├── encryption.go ├── exception.go ├── go.mod ├── go.sum ├── main.go ├── monument_data_structure.go ├── shamir_algorithm.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .idea/ 3 | 4 | # Created by https://www.gitignore.io/api/go,macos,windows,intellij,visualstudiocode 5 | # Edit at https://www.gitignore.io/?templates=go,macos,windows,intellij,visualstudiocode 6 | 7 | ### Go ### 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | ### Go Patch ### 25 | /vendor/ 26 | /Godeps/ 27 | 28 | ### Intellij ### 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 30 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 31 | 32 | # User-specific stuff 33 | .idea/**/workspace.xml 34 | .idea/**/tasks.xml 35 | .idea/**/usage.statistics.xml 36 | .idea/**/dictionaries 37 | .idea/**/shelf 38 | 39 | # Generated files 40 | .idea/**/contentModel.xml 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # Gradle and Maven with auto-import 56 | # When using Gradle or Maven with auto-import, you should exclude module files, 57 | # since they will be recreated, and may cause churn. Uncomment if using 58 | # auto-import. 59 | # .idea/modules.xml 60 | # .idea/*.iml 61 | # .idea/modules 62 | # *.iml 63 | # *.ipr 64 | 65 | # CMake 66 | cmake-build-*/ 67 | 68 | # Mongo Explorer plugin 69 | .idea/**/mongoSettings.xml 70 | 71 | # File-based project format 72 | *.iws 73 | 74 | # IntelliJ 75 | out/ 76 | 77 | # mpeltonen/sbt-idea plugin 78 | .idea_modules/ 79 | 80 | # JIRA plugin 81 | atlassian-ide-plugin.xml 82 | 83 | # Cursive Clojure plugin 84 | .idea/replstate.xml 85 | 86 | # Crashlytics plugin (for Android Studio and IntelliJ) 87 | com_crashlytics_export_strings.xml 88 | crashlytics.properties 89 | crashlytics-build.properties 90 | fabric.properties 91 | 92 | # Editor-based Rest Client 93 | .idea/httpRequests 94 | 95 | # Android studio 3.1+ serialized cache file 96 | .idea/caches/build_file_checksums.ser 97 | 98 | ### Intellij Patch ### 99 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 100 | 101 | # *.iml 102 | # modules.xml 103 | # .idea/misc.xml 104 | # *.ipr 105 | 106 | # Sonarlint plugin 107 | .idea/**/sonarlint/ 108 | 109 | # SonarQube Plugin 110 | .idea/**/sonarIssues.xml 111 | 112 | # Markdown Navigator plugin 113 | .idea/**/markdown-navigator.xml 114 | .idea/**/markdown-navigator/ 115 | 116 | ### macOS ### 117 | # General 118 | .DS_Store 119 | .AppleDouble 120 | .LSOverride 121 | 122 | # Icon must end with two \r 123 | Icon 124 | 125 | # Thumbnails 126 | ._* 127 | 128 | # Files that might appear in the root of a volume 129 | .DocumentRevisions-V100 130 | .fseventsd 131 | .Spotlight-V100 132 | .TemporaryItems 133 | .Trashes 134 | .VolumeIcon.icns 135 | .com.apple.timemachine.donotpresent 136 | 137 | # Directories potentially created on remote AFP share 138 | .AppleDB 139 | .AppleDesktop 140 | Network Trash Folder 141 | Temporary Items 142 | .apdisk 143 | 144 | ### VisualStudioCode ### 145 | .vscode/* 146 | !.vscode/settings.json 147 | !.vscode/tasks.json 148 | !.vscode/launch.json 149 | !.vscode/extensions.json 150 | 151 | ### VisualStudioCode Patch ### 152 | # Ignore all local history of files 153 | .history 154 | 155 | ### Windows ### 156 | # Windows thumbnail cache files 157 | Thumbs.db 158 | Thumbs.db:encryptable 159 | ehthumbs.db 160 | ehthumbs_vista.db 161 | 162 | # Dump file 163 | *.stackdump 164 | 165 | # Folder config file 166 | [Dd]esktop.ini 167 | 168 | # Recycle Bin used on file shares 169 | $RECYCLE.BIN/ 170 | 171 | # Windows Installer files 172 | *.cab 173 | *.msi 174 | *.msix 175 | *.msm 176 | *.msp 177 | 178 | # Windows shortcuts 179 | *.lnk 180 | 181 | # End of https://www.gitignore.io/api/go,macos,windows,intellij,visualstudiocode 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 James Swineson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # monument 2 | 3 | Allow a file to be decrypted when and only when you die. 4 | 5 | ## Project Status 6 | 7 | Under heavy development, no backward compatibility is guaranteed. Please use the same version for encryption & decryption. Do not use in production. 8 | 9 | ## Design 10 | 11 | A successful decryption of a monument-encrypted file requires all the following conditions to be true: 12 | 13 | * The encrypted version of your secret file is accessible 14 | * Your death, incapacitation or any trigger that you need to set up beforehand has been triggered (A Dead Man's Switch, referred as DMS later) 15 | * Keys from _k_ out of the _n_ key holders are gathered 16 | 17 | At encryption, monument generates a new PGP keypair, encrypts your file and splits the private key using Shamir's Secret Sharing Algorithm. You can designate how many people you will give one key to, and how many people are required to finally decrypt your file. You will receive _n_ keys, one for every person, plus _m_ keys to put into your DMS service. 18 | 19 | After encryption, you need to set up your DMS service in a way that it will send all the _m_ keys to every of the _n_ people, tell them about your death and tell them how to contact each other. 20 | 21 | When you die and the DMS service successfully triggers, if _k_ out of the _n_ people managed to have contact, they will have _k + m_ keys in total which will allow monument to finally decrypt your secret file. 22 | 23 | ## Usage 24 | 25 | ### Encryption phase 26 | 27 | For example, if you have 5 people to give keys to, and 3 of them are required to decrypt the secret: 28 | 29 | ```shell 30 | monument encrypt --name "Your Legal Name" --email "your.email@example.com" --people 5 --decryptable 3 --file secret-message.txt --output out 31 | ``` 32 | 33 | All the files required for decryption will be put into `out` directory. You need to: 34 | 35 | * Publish `public/secret-message.txt.gpg` to a place where it will be available even if you die 36 | * Put the content of `secret/shares_for_death_switch.txt` to your DMS service 37 | * Hand out the keys in `secret/shares_for_people.txt`, one key per person 38 | * Delete your original `secret-message.txt` file and all the keys in `secret/*` 39 | 40 | ### Decryption phase 41 | 42 | First download the encrypted `secret-message.txt.gpg`. Then run monument to start the decryption phase: 43 | 44 | ```shell 45 | monument decrypt --file secret-message.txt.gpg 46 | ``` 47 | 48 | Monument will then ask you for the keys you gathered. Paste one key per line. If the keys are correct, the secret will be revealed. -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 5m 4 | 5 | # include test files or not, default is true 6 | tests: true 7 | 8 | # default is true. Enables skipping of directories: 9 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 10 | skip-dirs-use-default: true 11 | 12 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 13 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 14 | # automatic updating of go.mod described above. Instead, it fails when any changes 15 | # to go.mod are needed. This setting is most useful to check that go.mod does 16 | # not need updates, such as in a continuous integration and testing system. 17 | # If invoked with -mod=vendor, the go command assumes that the vendor 18 | # directory holds the correct copies of dependencies and ignores 19 | # the dependency descriptions in go.mod. 20 | modules-download-mode: readonly 21 | 22 | # all available settings of specific linters 23 | linters-settings: 24 | errcheck: 25 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 26 | # default is false: such cases aren't reported by default. 27 | check-type-assertions: true 28 | 29 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 30 | # default is false: such cases aren't reported by default. 31 | check-blank: true 32 | 33 | govet: 34 | # report about shadowed variables 35 | check-shadowing: true 36 | 37 | # settings per analyzer 38 | settings: 39 | printf: # analyzer name, run `go tool vet help` to see all analyzers 40 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 41 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 42 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 43 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 44 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 45 | golint: 46 | # minimal confidence for issues, default is 0.8 47 | min-confidence: 0.8 48 | gofmt: 49 | # simplify code: gofmt with `-s` option, true by default 50 | simplify: true 51 | goimports: 52 | # put imports beginning with prefix after 3rd-party packages; 53 | # it's a comma-separated list of prefixes 54 | local-prefixes: github.com/Jamesits/SND 55 | gocyclo: 56 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 57 | min-complexity: 10 58 | gocognit: 59 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 60 | min-complexity: 10 61 | maligned: 62 | # print struct with more effective memory layout or not, false by default 63 | suggest-new: true 64 | dupl: 65 | # tokens count to trigger issue, 150 by default 66 | threshold: 100 67 | goconst: 68 | # minimal length of string constant, 3 by default 69 | min-len: 3 70 | # minimal occurrences count to trigger, 3 by default 71 | min-occurrences: 2 72 | depguard: 73 | list-type: blacklist 74 | include-go-root: false 75 | packages: 76 | - github.com/sirupsen/logrus 77 | packages-with-error-messages: 78 | # specify an error message to output when a blacklisted package is used 79 | github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 80 | unused: 81 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 82 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 83 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 84 | # with golangci-lint call it on a directory with the changed file. 85 | check-exported: false 86 | unparam: 87 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 88 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 89 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 90 | # with golangci-lint call it on a directory with the changed file. 91 | check-exported: false 92 | nakedret: 93 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 94 | max-func-lines: 30 95 | prealloc: 96 | # XXX: we don't recommend using this linter before doing performance profiling. 97 | # For most programs usage of prealloc will be a premature optimization. 98 | 99 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 100 | # True by default. 101 | simple: true 102 | range-loops: true # Report preallocation suggestions on range loops, true by default 103 | for-loops: false # Report preallocation suggestions on for loops, false by default 104 | gocritic: 105 | enabled-tags: 106 | - diagnostic 107 | - experimental 108 | - opinionated 109 | - performance 110 | - style 111 | disabled-checks: 112 | - wrapperFunc 113 | - dupImport # https://github.com/go-critic/go-critic/issues/845 114 | - ifElseChain 115 | - octalLiteral 116 | - emptyStringTest 117 | dogsled: 118 | # checks assignments with too many blank identifiers; default is 2 119 | max-blank-identifiers: 2 120 | whitespace: 121 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 122 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 123 | wsl: 124 | # If true append is only allowed to be cuddled if appending value is 125 | # matching variables, fields or types on line above. Default is true. 126 | strict-append: true 127 | # Allow calls and assignments to be cuddled as long as the lines have any 128 | # matching variables, fields or types. Default is true. 129 | allow-assign-and-call: true 130 | # Allow multiline assignments to be cuddled. Default is true. 131 | allow-multiline-assign: true 132 | # Allow declarations (var) to be cuddled. 133 | allow-cuddle-declarations: false 134 | # Allow trailing comments in ending of blocks 135 | allow-trailing-comment: false 136 | # Force newlines in end of case at this limit (0 = never). 137 | force-case-trailing-whitespace: 0 138 | 139 | linters: 140 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 141 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 142 | disable-all: true 143 | enable: 144 | - bodyclose 145 | - deadcode 146 | - depguard 147 | - dogsled 148 | - dupl 149 | - errcheck 150 | - gochecknoinits 151 | - goconst 152 | - gocritic 153 | - gocyclo 154 | - gofmt 155 | - goimports 156 | - golint 157 | - gosec 158 | - gosimple 159 | - govet 160 | - ineffassign 161 | - interfacer 162 | - nakedret 163 | - scopelint 164 | - staticcheck 165 | - structcheck 166 | - stylecheck 167 | - typecheck 168 | - unconvert 169 | - unparam 170 | - unused 171 | - varcheck 172 | - whitespace 173 | - gocognit 174 | - maligned 175 | - prealloc 176 | 177 | issues: 178 | # Excluding configuration per-path, per-linter, per-text and per-source 179 | exclude-rules: 180 | # Exclude some linters from running on tests files. 181 | - path: _test\.go 182 | linters: 183 | - gocyclo 184 | - errcheck 185 | - dupl 186 | - gosec 187 | 188 | # Exclude known linters from partially hard-vendored code, 189 | # which is impossible to exclude via "nolint" comments. 190 | - path: internal/hmac/ 191 | text: "weak cryptographic primitive" 192 | linters: 193 | - gosec 194 | 195 | # Exclude lll issues for long lines with go:generate 196 | - linters: 197 | - lll 198 | source: "^//go:generate " 199 | 200 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 201 | max-issues-per-linter: 0 202 | 203 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 204 | max-same-issues: 0 205 | -------------------------------------------------------------------------------- /src/encryption.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "golang.org/x/crypto/openpgp" 6 | "golang.org/x/crypto/openpgp/armor" 7 | "golang.org/x/crypto/openpgp/packet" 8 | _ "golang.org/x/crypto/ripemd160" 9 | "io" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | const ( 15 | md5 = 1 16 | sha1 = 2 17 | ripemd160 = 3 18 | sha256 = 8 19 | sha384 = 9 20 | sha512 = 10 21 | sha224 = 11 22 | ) 23 | 24 | /* 25 | Minimal operation to initialize a new monument: 26 | 27 | 1. generate an OpenPGP keypair 28 | 2. save the public key 29 | 3. distribute the private key via Shamir's Secret Sharing algorithm 30 | */ 31 | 32 | func initMonument(monument *monument) { 33 | monument.createdAt = time.Now() 34 | 35 | // generate an OpenPGP keypair 36 | var err error 37 | monument.pgpEntity, err = openpgp.NewEntity(monument.ownerName, "", monument.ownerEmail, nil) 38 | hardFailIf(err) 39 | 40 | // Sign all the identities 41 | 42 | // dur := uint32(config.Expiry.Seconds()) 43 | for _, id := range monument.pgpEntity.Identities { 44 | // id.SelfSignature.KeyLifetimeSecs = &dur 45 | 46 | id.SelfSignature.PreferredSymmetric = []uint8{ 47 | uint8(packet.CipherAES256), 48 | uint8(packet.CipherAES192), 49 | uint8(packet.CipherAES128), 50 | uint8(packet.CipherCAST5), 51 | uint8(packet.Cipher3DES), 52 | } 53 | 54 | id.SelfSignature.PreferredHash = []uint8{ 55 | sha256, 56 | sha1, 57 | sha384, 58 | sha512, 59 | sha224, 60 | } 61 | 62 | id.SelfSignature.PreferredCompression = []uint8{ 63 | uint8(packet.CompressionZLIB), 64 | uint8(packet.CompressionZIP), 65 | } 66 | 67 | err = id.SelfSignature.SignUserId(id.UserId.Id, monument.pgpEntity.PrimaryKey, monument.pgpEntity.PrivateKey, nil) 68 | hardFailIf(err) 69 | } 70 | 71 | // Self-sign the Subkeys 72 | // https://github.com/alokmenghrajani/gpgeez/blob/master/gpgeez.go 73 | for _, subkey := range monument.pgpEntity.Subkeys { 74 | // subkey.Sig.KeyLifetimeSecs 75 | err := subkey.Sig.SignKey(subkey.PublicKey, monument.pgpEntity.PrivateKey, nil) 76 | hardFailIf(err) 77 | } 78 | 79 | //w, err := armor.Encode(os.Stdout, openpgp.PublicKeyType, nil) 80 | //hardFailIf(err) 81 | //defer w.Close() 82 | // 83 | //monument.pgpEntity.Serialize(w) 84 | 85 | // serialize the private key 86 | var privateKeyBuffer bytes.Buffer 87 | w, err := armor.Encode(&privateKeyBuffer, openpgp.PrivateKeyType, generateKeyBlockHeader(monument)) 88 | hardFailIf(err) 89 | err = monument.pgpEntity.SerializePrivate(w, nil) 90 | hardFailIf(err) 91 | err = w.Close() 92 | hardFailIf(err) 93 | 94 | // create Shamir shares of the serialized private key 95 | monument.totalShares, monument.minimalShares, monument.deadSwitchShares = getPeopleCount(monument.totalPeople, monument.minimalPeople) 96 | monument.shamirShares = shamirEncrypt(privateKeyBuffer.String(), monument.totalShares, monument.minimalShares) 97 | } 98 | 99 | func generateKeyBlockHeader(monument *monument) (ret map[string]string) { 100 | ret = map[string]string{ 101 | "CreatedBy": getVersionFullString(), 102 | "CreatedAt": monument.createdAt.String() + " (" + strconv.FormatInt(monument.createdAt.UTC().UnixNano(), 10) + ")", 103 | } 104 | 105 | return 106 | } 107 | 108 | // note: RIPEMD160 is used by default 109 | // https://github.com/golang/go/issues/12153 110 | func encryptData(recipients []*openpgp.Entity, signer *openpgp.Entity, r io.Reader, w io.Writer) error { 111 | wc, err := openpgp.Encrypt(w, recipients, signer, &openpgp.FileHints{IsBinary: true}, nil) 112 | if err != nil { 113 | return err 114 | } 115 | if _, err := io.Copy(wc, r); err != nil { 116 | return err 117 | } 118 | return wc.Close() 119 | } 120 | 121 | func exportPublicKey(monument *monument, writer io.Writer) { 122 | var err error 123 | armoredWriter, err := armor.Encode(writer, openpgp.PublicKeyType, generateKeyBlockHeader(monument)) 124 | hardFailIf(err) 125 | defer armoredWriter.Close() 126 | 127 | err = monument.pgpEntity.Serialize(armoredWriter) 128 | hardFailIf(err) 129 | } 130 | 131 | func decryptData() { 132 | 133 | } -------------------------------------------------------------------------------- /src/exception.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // if QuitOnError is true, then panic; 8 | // else go on 9 | func softFailIf(e error) { 10 | if e != nil { 11 | log.Printf("[WARNING] %s", e) 12 | } 13 | } 14 | 15 | func hardFailIf(e error) { 16 | if e != nil { 17 | log.Printf("[ERROR] %s", e) 18 | panic(e) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Jamesits/monument 2 | 3 | go 1.13 4 | 5 | replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0 6 | 7 | require ( 8 | github.com/SSSaaS/sssa-golang v0.0.0-20170502204618-d37d7782d752 9 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 10 | ) 11 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0 h1:mCww5Yl0Pm4PZPSooupyWDgihrh96p6+O4PY1hs0FBw= 2 | github.com/ProtonMail/crypto v0.0.0-20191122234321-e77a1f03baa0/go.mod h1:MBriIAodHvZ+YvwvMJWCTmseW/LkeVRPWp/iZKvee4g= 3 | github.com/SSSaaS/sssa-golang v0.0.0-20170502204618-d37d7782d752 h1:NMpC6M+PtNNDYpq7ozB7kINpv10L5yeli5GJpka2PX8= 4 | github.com/SSSaaS/sssa-golang v0.0.0-20170502204618-d37d7782d752/go.mod h1:PbJ8S5YaSYAvDPTiEuUsBHQwTUlPs6VM+Av8Oi3v570= 5 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 6 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= 7 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 8 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 9 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 10 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 12 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "github.com/SSSaaS/sssa-golang" 9 | "golang.org/x/crypto/openpgp" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "path" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | func main() { 20 | flag.CommandLine.SetOutput(os.Stderr) 21 | flag.Usage = func() { 22 | flag.PrintDefaults() 23 | } 24 | 25 | if len(os.Args) < 2 { 26 | subCommandDoNotMatch() 27 | os.Exit(1) 28 | } 29 | 30 | switch strings.ToLower(os.Args[1]) { 31 | case "encrypt": 32 | doEncryption() 33 | case "decrypt": 34 | doDecryption() 35 | default: 36 | subCommandDoNotMatch() 37 | os.Exit(1) 38 | } 39 | } 40 | 41 | func printHelpHeader() { 42 | fmt.Fprintf(os.Stderr, "monument %s \nhttps://github.com/Jamesits/monument\n\n", getVersionNumberString()) 43 | // fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0])) 44 | } 45 | 46 | func subCommandDoNotMatch() { 47 | printHelpHeader() 48 | fmt.Fprintf(os.Stderr, "Available subcommands: \n\tencrypt\n\tdecrypt\n") 49 | } 50 | 51 | func doEncryption() { 52 | var err error 53 | // parse command line 54 | encryptionCmdLineFlags := flag.NewFlagSet("encrypt", flag.ExitOnError) 55 | name := encryptionCmdLineFlags.String("name", "", "Your legal name") 56 | email := encryptionCmdLineFlags.String("email", "", "Your email") 57 | encryptionFilePath := encryptionCmdLineFlags.String("file", "-", "Path to the file you want to encrypt") 58 | totalPeople := encryptionCmdLineFlags.Int("people", 0, "How many people in total do you need to give part of the key") 59 | requiredPeople := encryptionCmdLineFlags.Int("decryptable", 0, "How many people is required to decrypt the file") 60 | outputDir := encryptionCmdLineFlags.String("output", "output", "A directory where all files you need will be put") 61 | 62 | err = encryptionCmdLineFlags.Parse(os.Args[2:]) 63 | hardFailIf(err) 64 | 65 | // TODO: check arguments 66 | 67 | // create output directory 68 | publicDirPath := path.Join(*outputDir, "public") 69 | secretDirPath := path.Join(*outputDir, "secret") 70 | err = os.MkdirAll(publicDirPath, 0700) 71 | hardFailIf(err) 72 | err = os.MkdirAll(secretDirPath, 0700) 73 | hardFailIf(err) 74 | 75 | // init monument internal data 76 | m := monument{ 77 | ownerName: *name, 78 | ownerEmail: *email, 79 | totalPeople: *totalPeople, 80 | minimalPeople: *requiredPeople, 81 | } 82 | 83 | initMonument(&m) 84 | 85 | log.Printf("\nWill generate %d keys, %d for distribution and %d for death switch\nAllow decryption at %d keys or %d people\n", m.totalShares, m.totalPeople, m.deadSwitchShares, m.minimalShares, m.minimalPeople) 86 | 87 | // encrypt file 88 | // TODO: support stdin 89 | encryptionFileReader, err := os.Open(*encryptionFilePath) 90 | if err != nil { 91 | fmt.Println(err) 92 | return 93 | } 94 | defer encryptionFileReader.Close() 95 | 96 | encryptionFileWriter, err := os.Create(path.Join(publicDirPath, filepath.Base(*encryptionFilePath)+".gpg")) 97 | if err != nil { 98 | fmt.Println(err) 99 | return 100 | } 101 | defer encryptionFileWriter.Close() 102 | 103 | err = encryptData([]*openpgp.Entity{m.pgpEntity}, m.pgpEntity, encryptionFileReader, encryptionFileWriter) 104 | hardFailIf(err) 105 | 106 | // output pubkey 107 | publicKeyWriter, err := os.Create(path.Join(publicDirPath, "pubkey.gpg")) 108 | if err != nil { 109 | fmt.Println(err) 110 | return 111 | } 112 | defer publicKeyWriter.Close() 113 | exportPublicKey(&m, publicKeyWriter) 114 | 115 | // output shares for the death switch 116 | sharesForDeathSwitch, err := os.Create(path.Join(secretDirPath, "shares_for_death_switch.txt")) 117 | if err != nil { 118 | fmt.Println(err) 119 | return 120 | } 121 | defer sharesForDeathSwitch.Close() 122 | 123 | _, err = fmt.Fprintf(sharesForDeathSwitch, "Put all the following lines into a \"dead man's switch\" service: \n\n") 124 | hardFailIf(err) 125 | for _, share := range m.shamirShares[0 : m.deadSwitchShares-1] { 126 | _, err = fmt.Fprintln(sharesForDeathSwitch, share) 127 | hardFailIf(err) 128 | } 129 | 130 | // output shares for people 131 | sharesForPeopleWriter, err := os.Create(path.Join(secretDirPath, "shares_for_people.txt")) 132 | if err != nil { 133 | fmt.Println(err) 134 | return 135 | } 136 | defer sharesForPeopleWriter.Close() 137 | 138 | _, err = fmt.Fprintf(sharesForPeopleWriter, "Send each line to a different people: \n\n") 139 | hardFailIf(err) 140 | for _, share := range m.shamirShares[m.deadSwitchShares:] { 141 | _, err = fmt.Fprintln(sharesForPeopleWriter, share) 142 | hardFailIf(err) 143 | } 144 | } 145 | 146 | func doDecryption() { 147 | var err error 148 | // parse command line 149 | decryptionCmdLineFlags := flag.NewFlagSet("decrypt", flag.ExitOnError) 150 | encryptedFilePath := decryptionCmdLineFlags.String("file", "-", "Path to the file you want to decrypt") 151 | privateKeyPath := decryptionCmdLineFlags.String("private-key", "", "Use this provate key instead of reading from shares") 152 | 153 | err = decryptionCmdLineFlags.Parse(os.Args[2:]) 154 | hardFailIf(err) 155 | 156 | // open encrypted file 157 | encryptedFileReader, err := os.Open(*encryptedFilePath) 158 | if err != nil { 159 | fmt.Println(err) 160 | return 161 | } 162 | defer encryptedFileReader.Close() 163 | 164 | var privateKeyReader io.Reader 165 | var privateKeyBuffer string 166 | 167 | if len(*privateKeyPath) > 0 { 168 | // read private key 169 | privateKeyFileReader, err := os.Open(*privateKeyPath) 170 | if err != nil { 171 | fmt.Println(err) 172 | return 173 | } 174 | defer privateKeyFileReader.Close() 175 | privateKeyReader = privateKeyFileReader 176 | } else { 177 | // read shares 178 | var collectedShares []string 179 | 180 | // read shamir shares 181 | // TODO: support reading from a file or something 182 | fmt.Println("Please paste the keys, one key per line:") 183 | 184 | scanner := bufio.NewScanner(os.Stdin) 185 | for scanner.Scan() { 186 | // TODO: check if line is empty 187 | collectedShares = append(collectedShares, scanner.Text()) 188 | privateKeyBuffer, err = shamirDecrypt(collectedShares) 189 | 190 | if errors.Is(err, sssa.ErrOneOfTheSharesIsInvalid) { 191 | // invalid share 192 | _, err = fmt.Fprintln(os.Stderr, "Last key is invalid") 193 | hardFailIf(err) 194 | collectedShares = collectedShares[0 : len(collectedShares)-2] 195 | } 196 | 197 | if strings.Contains(privateKeyBuffer, "BEGIN PGP PRIVATE KEY BLOCK") { 198 | // yes we decrypted the block 199 | // fmt.Println(privateKeyBuffer) 200 | break 201 | } 202 | } 203 | 204 | if err := scanner.Err(); err != nil { 205 | log.Println(err) 206 | } 207 | 208 | privateKeyReader = strings.NewReader(privateKeyBuffer) 209 | } 210 | 211 | // https://gist.github.com/stuart-warren/93750a142d3de4e8fdd2 212 | // TODO: check if privateKeyBuffer is valid 213 | var entityList openpgp.EntityList 214 | entityList, err = openpgp.ReadArmoredKeyRing(privateKeyReader) 215 | hardFailIf(err) 216 | 217 | // assert len(entityList) > 0 218 | // fmt.Printf("Entities: %d\n", len(entityList)) 219 | 220 | for key, _ := range entityList[0].Identities { 221 | // here key == value.name 222 | fmt.Printf("Decrypted identity: %s\n", key) 223 | } 224 | 225 | // Decrypt it with the contents of the private key 226 | md, err := openpgp.ReadMessage(encryptedFileReader, entityList, nil, nil) 227 | hardFailIf(err) 228 | bytes, err := ioutil.ReadAll(md.UnverifiedBody) 229 | hardFailIf(err) 230 | fmt.Println("Contents: ") 231 | fmt.Println(string(bytes)) 232 | } 233 | -------------------------------------------------------------------------------- /src/monument_data_structure.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/crypto/openpgp" 5 | "time" 6 | ) 7 | 8 | type monument struct { 9 | // user configurable 10 | ownerName string 11 | ownerEmail string 12 | totalPeople int 13 | minimalPeople int 14 | 15 | // auto generated 16 | totalShares int 17 | minimalShares int 18 | deadSwitchShares int 19 | pgpEntity *openpgp.Entity 20 | shamirShares []string 21 | 22 | createdAt time.Time 23 | } 24 | -------------------------------------------------------------------------------- /src/shamir_algorithm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/SSSaaS/sssa-golang" 5 | ) 6 | 7 | /* 8 | Shamir algo total keys a, required b 9 | People involved total x, required y (assume everyone have 1 key) 10 | death switch have z keys 11 | 12 | constraints: 13 | x+z<=a 14 | y+z>=b 15 | y+z-1=b>0 20 | x>=y>0 21 | z>0 22 | 23 | So we have: (in Wolfram language) 24 | Reduce[{x + z <= a, x < b, y + z >= b, y + z - 1 < b, x < b, z < b, a >= b, x >= y, b > 0, y > 0, z > 0}, {a, b, x, y, z}, Integers] 25 | 26 | which resulted in: 27 | a = n_1 + 2 n_2 + n_3 + n_4 + 2, b = n_1 + n_2 + n_3 + 2, x = n_1 + n_2 + 1, y = n_1 + 1, z = n_2 + n_3 + 1, n_4 element Z, n_4 >=0, n_3 element Z, n_3 >=0, n_2 element Z, n_2 >=0, n_1 element Z, n_1 >=0 28 | */ 29 | 30 | func getPeopleCount(totalPeople int, requiredPeople int) (totalShares int, requiredShares int, deathSwitchShares int) { 31 | // TODO: sanity checks 32 | 33 | n2 := totalPeople - requiredPeople 34 | n1 := requiredPeople - 1 35 | n3 := 0 // const >=0 36 | n4 := 0 // const >=0 37 | deathSwitchShares = n2 + n3 + 1 38 | requiredShares = n1 + n2 + n3 + 2 39 | totalShares = n1 + 2*n2 + n3 + n4 + 2 40 | 41 | return 42 | } 43 | 44 | func shamirEncrypt(data string, totalShares int, requiredShares int) []string { 45 | ret, err := sssa.Create(requiredShares, totalShares, data) 46 | hardFailIf(err) 47 | 48 | return ret 49 | } 50 | 51 | func shamirDecrypt(collectedShares []string) (string, error) { 52 | ret, err := sssa.Combine(collectedShares) 53 | 54 | return ret, err 55 | } 56 | -------------------------------------------------------------------------------- /src/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | var versionMajor = 0 6 | var versionMinor = 0 7 | var versionRevision = 1 8 | var versionGitCommitHash string 9 | var versionCompileTime string 10 | var versionCompileHost string 11 | var versionGitStatus string 12 | 13 | func getVersionNumberString() string { 14 | return fmt.Sprintf("%d.%d.%d", versionMajor, versionMinor, versionRevision) 15 | } 16 | 17 | func getVersionFullString() string { 18 | if len(versionCompileHost) == 0 { 19 | versionCompileHost = "localhost" 20 | } 21 | 22 | if len(versionGitCommitHash) == 0 { 23 | versionGitCommitHash = "UNKNOWN" 24 | } 25 | 26 | if len(versionCompileTime) == 0 { 27 | versionCompileTime = "UNKNOWN TIME" 28 | } 29 | 30 | if len(versionGitStatus) == 0 { 31 | versionGitStatus = "dirty" 32 | } 33 | 34 | return fmt.Sprintf("monument %s (https://github.com/Jamesits/monument, compiled on %s for commit %s (%s) at %s)", getVersionNumberString(), versionCompileHost, versionGitCommitHash, versionGitStatus, versionCompileTime) 35 | } --------------------------------------------------------------------------------