diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bf02a5a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: bug, needs triage +assignees: dustin-decker, dxa4481 +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..89473ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement, needs triage +assignees: dustin-decker, dxa4481 +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..31c7b80 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,4 @@ + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..76c7c84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: release + +on: + push: + tags: + - 'v*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2a603b1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at community@trufflesec.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..906d4c5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +# Contribution guidelines + +Please create an issue to collect feedback prior to feature additions. +If possible try to keep PR's scoped to one feature, and add tests for new features. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e36c857 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:bullseye as builder +RUN mkdir /build +COPY . /build +WORKDIR /build +RUN CGO_ENABLED=0 go build -a -o driftwood main.go + +FROM scratch +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /build/driftwood /usr/bin/driftwood +ENTRYPOINT ["/usr/bin/driftwood"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..85f9e9a --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Driftwood + +[![CI Status](https://github.com/trufflesecurity/driftwood/workflows/release/badge.svg)](https://github.com/trufflesecurity/driftwood/actions) +[![Go Report Card](https://goreportcard.com/badge/github.com/trufflesecurity/driftwood)](https://goreportcard.com/report/github.com/trufflesecurity/driftwood) +[![Docker Hub Build Status](https://img.shields.io/docker/cloud/build/trufflesecurity/driftwood.svg)](https://hub.docker.com/r/trufflesecurity/driftwood/) +![GitHub](https://img.shields.io/github/license/trufflesecurity/driftwood) + +Driftwood is a tool that can enable you to lookup whether a private key is used for things like TLS or as a GitHub SSH key for a user. + +Driftwood performs lookups with the computed public key, so the private key never leaves where you run the tool. Additionally it supports some basic password cracking for encrypted keys. + +![Driftwood in action](docs/screenshot.png) + +## Installation + +Three easy ways to get started. + +### Run with Docker + +```bash +cat private.key | docker run --rm -i trufflesecurity/driftwood --pretty-json - +``` + +### Run pre-built binary +Download the binary from the [releases page](https://github.com/trufflesecurity/driftwood/releases) and run it. + +### Build yourself + +```bash +go install github.com/trufflesecurity/driftwood@latest +``` + +## Usage + +Minimal usage is + +```bash +$ driftwood path/to/privatekey.pem +``` + +Run with `--help` to see more options. + +## Library Usage + +Packages under `pkg/` are libraries that can be used for external consumption. Packages under `pkg/exp/` are considered to be experimental status and may have breaking changes. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6530143 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Please report any security issues to security@trufflesec.com and include `driftwood` in the subject \ No newline at end of file diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000..7f5fbbd Binary files /dev/null and b/docs/screenshot.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3bdf23c --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/trufflesecurity/driftwood + +go 1.17 + +require ( + github.com/mitchellh/mapstructure v1.4.2 + github.com/sirupsen/logrus v1.8.1 + github.com/urfave/cli/v2 v2.3.0 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..077c5df --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4b5d90a --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/trufflesecurity/driftwood/pkg/exp/client" + "github.com/trufflesecurity/driftwood/pkg/exp/parser" +) + +var version = "dev" + +func main() { + app := &cli.App{ + Name: "driftwood", + Usage: "Verify if a private key is used for important things.", + UsageText: "driftwood ", + Version: version, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "pretty-json", + }, + &cli.BoolFlag{ + Name: "debug", + }, + }, + Action: func(c *cli.Context) error { + args := c.Args().Slice() + + logger := logrus.New() + logger.SetOutput(os.Stderr) + if c.Bool("json") { + logger.SetFormatter(&logrus.JSONFormatter{ + DisableTimestamp: true, + }) + } else { + logger.SetFormatter(&logrus.TextFormatter{ + DisableTimestamp: true, + }) + } + + if c.Bool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + + if len(args) == 0 { + cli.ShowAppHelpAndExit(c, 1) + return nil + } + + file := args[0] + + var privateKey []byte + var err error + if file == "-" { + buff := bytes.NewBuffer(nil) + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + buff.WriteString(scanner.Text() + "\n") + } + privateKey = buff.Bytes() + } else { + privateKey, err = ioutil.ReadFile(args[0]) + if err != nil { + logger.Fatalf("File cannot be read: %s", err) + return nil + } + } + + publicKey, err := parser.PublicKey(privateKey) + if err != nil { + logger.Fatalf("Error computing public key: %s", err) + } + result, err := client.Lookup(version, publicKey) + if err != nil { + logger.Fatalf("Error looking up public key: %s", err) + } + + if !c.Bool("pretty-json") { + out, err := json.Marshal(result) + if err != nil { + logger.Fatalf("Error marshalling result: %s", err) + } + fmt.Println(string(out)) + } else { + out, err := json.MarshalIndent(result, "", "\t") + if err != nil { + logger.Fatalf("Error marshalling result: %s", err) + } + fmt.Println(string(out)) + } + + return nil + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/exp/client/client.go b/pkg/exp/client/client.go new file mode 100644 index 0000000..72c7040 --- /dev/null +++ b/pkg/exp/client/client.go @@ -0,0 +1,128 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + "reflect" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/sirupsen/logrus" +) + +var ( + client = &http.Client{ + Timeout: time.Second * 3, + Transport: http.DefaultTransport, + } +) + +func Lookup(version string, publicKey []byte) (result Result, err error) { + req, err := http.NewRequest("POST", "https://keychecker.trufflesecurity.com/publickey", nil) + if err != nil { + return + } + req.Header.Set("User-Agent", fmt.Sprintf("Driftwood %s", version)) + var body io.ReadCloser + for { + req.Body = io.NopCloser(bytes.NewBuffer(publicKey)) + res, err := client.Do(req) + if err != nil { + return result, err + } + if logrus.GetLevel() == logrus.DebugLevel { + o, _ := httputil.DumpResponse(res, true) + fmt.Println(string(o)) + } + if res.StatusCode == http.StatusTooManyRequests || res.StatusCode == http.StatusServiceUnavailable { + if s, ok := res.Header["Retry-After"]; ok { + var retryTime time.Time + if retryTime, err = time.Parse(time.RFC1123, s[0]); err == nil { + wait := time.Until(retryTime.Add(time.Second * 3)) + logrus.Infof("Hit rate limit, retrying in %d seconds", int(wait.Seconds())) + time.Sleep(wait) + continue + } + } + } + if res.StatusCode == http.StatusOK { + body = res.Body + defer res.Body.Close() + break + } + } + + rawMap := map[string]interface{}{} + err = json.NewDecoder(body).Decode(&rawMap) + if err != nil { + return + } + var md mapstructure.Metadata + decoder, err := mapstructure.NewDecoder( + &mapstructure.DecoderConfig{ + Metadata: &md, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + ToTimeHookFunc()), + Result: &result, + }) + if err != nil { + return + } + if err = decoder.Decode(rawMap); err != nil { + return + } + result.X = map[string]interface{}{} + for _, k := range md.Unused { + result.X[k] = rawMap[k] + } + + for i := range result.CertificateResults { + result.CertificateResults[i].VerificationURL = fmt.Sprintf("https://crt.sh/?q=%s", result.CertificateResults[i].CertificateFingerprint) + } + + return +} + +func ToTimeHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if t != reflect.TypeOf(time.Time{}) { + return data, nil + } + + switch f.Kind() { + case reflect.String: + return time.Parse(time.RFC3339, data.(string)) + case reflect.Float64: + return time.Unix(0, int64(data.(float64))*int64(time.Millisecond)), nil + case reflect.Int64: + return time.Unix(0, data.(int64)*int64(time.Millisecond)), nil + default: + return data, nil + } + // Convert it by parsing + } +} + +type CertificateResult struct { + CertificateFingerprint string + ExpirationTimestamp time.Time + VerificationURL string +} + +type GitHubSSHResult struct { + Username string +} + +type Result struct { + CertificateResults []CertificateResult `json:",omitempty"` + GitHubSSHResults []GitHubSSHResult `json:",omitempty"` + Error string `json:",omitempty"` + X map[string]interface{} `json:",omitempty"` +} diff --git a/pkg/exp/cracker/cracker.go b/pkg/exp/cracker/cracker.go new file mode 100644 index 0000000..d700db7 --- /dev/null +++ b/pkg/exp/cracker/cracker.go @@ -0,0 +1,39 @@ +package cracker + +import ( + "bytes" + "crypto/x509" + _ "embed" + "errors" + + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" +) + +//go:embed "top250.txt" +var rawCrackList []byte +var passphrases [][]byte + +func init() { + passphrases = bytes.Split(rawCrackList, []byte("\n")) +} + +var ( + ErrUncrackable = errors.New("unable to crack encryption") +) + +func Crack(in []byte) (interface{}, error) { + for _, passphrase := range passphrases { + parsed, err := ssh.ParseRawPrivateKeyWithPassphrase(in, passphrase) + if err != nil { + if errors.Is(err, x509.IncorrectPasswordError) { + continue + } else { + return nil, err + } + } + logrus.Infof("🔓 Cracked key with passphrase: %s", string(passphrase)) + return parsed, nil + } + return nil, ErrUncrackable +} diff --git a/pkg/exp/cracker/cracker_test.go b/pkg/exp/cracker/cracker_test.go new file mode 100644 index 0000000..6ff2b23 --- /dev/null +++ b/pkg/exp/cracker/cracker_test.go @@ -0,0 +1,93 @@ +package cracker + +import ( + _ "embed" + "testing" + + "golang.org/x/crypto/ssh" +) + +var ( + testEncryptedKeyCorrectPassword = []byte("123456") + testEncryptedKeyIncorrectPassword = []byte("incorrect") + testEncryptedKey = []byte(string(`-----BEGIN OPENSSH PRIVATE KEY----- +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 +Hx7UPVlTK8dyvk1Z+Yw0nrfNClI= +-----END OPENSSH PRIVATE KEY-----`)) +) + +func Test_Crack(t *testing.T) { + tests := []struct { + name string + in []byte + wantErr bool + }{ + { + name: "crackable", + wantErr: false, + in: testEncryptedKey, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Crack(tt.in) + if (err != nil) != tt.wantErr { + t.Errorf("crack() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func BenchmarkParseWrongPassword(b *testing.B) { + for n := 0; n < b.N; n++ { + ssh.ParseRawPrivateKeyWithPassphrase(testEncryptedKey, testEncryptedKeyIncorrectPassword) + } +} + +func BenchmarkParseRightPassword(b *testing.B) { + for n := 0; n < b.N; n++ { + ssh.ParseRawPrivateKeyWithPassphrase(testEncryptedKey, testEncryptedKeyCorrectPassword) + } +} + +func BenchmarkCracker(b *testing.B) { + for n := 0; n < b.N; n++ { + Crack(testEncryptedKey) + } +} diff --git a/pkg/exp/cracker/top250.txt b/pkg/exp/cracker/top250.txt new file mode 100644 index 0000000..3114049 --- /dev/null +++ b/pkg/exp/cracker/top250.txt @@ -0,0 +1,249 @@ +123456 +12345 +123456789 +password +iloveyou +princess +1234567 +12345678 +abc123 +nicole +daniel +babygirl +monkey +lovely +jessica +654321 +michael +ashley +qwerty +111111 +iloveu +000000 +michelle +tigger +sunshine +chocolate +password1 +soccer +anthony +friends +butterfly +purple +angel +jordan +liverpool +justin +loveme +fuckyou +123123 +football +secret +andrea +carlos +jennifer +joshua +bubbles +1234567890 +superman +hannah +amanda +loveyou +pretty +basketball +andrew +angels +tweety +flower +playboy +hello +elizabeth +hottie +tinkerbell +charlie +samantha +barbie +chelsea +lovers +teamo +jasmine +brandon +666666 +shadow +melissa +eminem +matthew +robert +danielle +forever +family +jonathan +987654321 +computer +whatever +dragon +vanessa +cookie +naruto +summer +sweety +spongebob +joseph +junior +softball +taylor +yellow +daniela +lauren +mickey +princesa +alexandra +alexis +jesus +estrella +miguel +william +thomas +beautiful +mylove +angela +poohbear +patrick +iloveme +sakura +adrian +alexander +destiny +christian +121212 +sayang +america +dancer +monica +richard +112233 +princess1 +555555 +diamond +carolina +steven +rangers +louise +orange +789456 +999999 +shorty +11111 +nathan +snoopy +gabriel +hunter +cherry +killer +sandra +alejandro +buster +george +brittany +alejandra +patricia +rachel +tequiero +7777777 +cheese +159753 +arsenal +dolphin +antonio +heather +david +ginger +stephanie +peanut +blink182 +sweetie +222222 +beauty +987654 +victoria +honey +00000 +fernando +pokemon +maggie +corazon +chicken +pepper +cristina +rainbow +kisses +manuel +myspace +rebelde +angel1 +ricardo +babygurl +heaven +55555 +baseball +martin +greenday +november +alyssa +madison +mother +123321 +123abc +mahalkita +batman +september +december +morgan +mariposa +maria +gabriela +iloveyou2 +bailey +jeremy +pamela +kimberly +gemini +shannon +pictures +asshole +sophie +jessie +hellokitty +claudia +babygirl1 +angelica +austin +mahalko +victor +horses +tiffany +mariana +eduardo +andres +courtney +booboo +kissme +harley +ronaldo +iloveyou1 +precious +october +inuyasha +peaches +veronica +chris +888888 +adriana +cutie +james +banana +prince +friend +jesus1 +crystal +celtic \ No newline at end of file diff --git a/pkg/exp/parser/parser.go b/pkg/exp/parser/parser.go new file mode 100644 index 0000000..9d51b69 --- /dev/null +++ b/pkg/exp/parser/parser.go @@ -0,0 +1,47 @@ +package parser + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "errors" + "strings" + + "golang.org/x/crypto/ssh" + + "github.com/sirupsen/logrus" + "github.com/trufflesecurity/driftwood/pkg/exp/cracker" +) + +var ( + ErrNotSupported = errors.New("key type not supported") + ErrEncryptedKey = errors.New("key is encrypted") +) + +func PublicKey(privateKey []byte) ([]byte, error) { + parsedKey, err := ssh.ParseRawPrivateKey(privateKey) + if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") { + logrus.Info("🔒 Encrypted key detected, attempting to crack it") + parsedKey, err = cracker.Crack(privateKey) + if err != nil { + return nil, err + } + } else if err != nil { + return nil, errors.New(strings.TrimPrefix(err.Error(), "ssh: ")) + } + + var pubKey interface{} + switch privateKey := parsedKey.(type) { + case *rsa.PrivateKey: + pubKey = &privateKey.PublicKey + case *ecdsa.PrivateKey: + pubKey = &privateKey.PublicKey + case ed25519.PrivateKey: + pubKey = privateKey.Public() + default: + return nil, ErrNotSupported + } + + return x509.MarshalPKIXPublicKey(pubKey) +}