diff --git a/README.rst b/README.rst index 9967ad9a1..266d8fcb8 100644 --- a/README.rst +++ b/README.rst @@ -392,6 +392,36 @@ Example: place the following in your `~/.bashrc` .. code:: bash SOPS_GPG_EXEC = 'your_gpg_client_wrapper' + +Shamir Secret Sharing +~~~~~~~~~~~~~~~~~~~~~ + +By default, `sops` encrypts the data key with each master keys, such that if any +of the master keys is available, the whole file can be decrypted. Sometimes, it +is desirable to require access to several master keys in order to be able to +decrypt files. This can be achieved with Shamir's Secret Sharing. With this +method, the data key is split into several parts, one for each master key, and +`quorum` parts are required in order to retrieve the data key and decrypt the file. + +You can enable this mode by passing `--shamir-secret-sharing` to the encrypt +mode or by passing it to the edit mode for new files. You can set `quorum` with +`--shamir-secret-sharing-quorum number`. The quorum must be lower or +equal to the number of master keys. + +`quorum` defaults to 2. + +For example: + +``` +sops --shamir-secret-sharing --shamir-secret-sharing-quorum 5 example.json +``` + +This will require at least 5 master keys in order to decrypt the file. You can +then decrypt the file the same way as with any other SOPS file: + +``` +sops -d example.json +``` Important information on types ------------------------------ diff --git a/cmd/sops/main.go b/cmd/sops/main.go index f832de9df..a45f57fab 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -76,6 +76,12 @@ func loadPlainFile(c *cli.Context, store sops.Store, fileName string, fileBytes Version: version, KeySources: ks, } + if c.Bool("shamir-secret-sharing") { + tree.Metadata.Shamir = true + } + if quorum := c.Int("shamir-secret-sharing-quorum"); quorum != 0 { + tree.Metadata.ShamirQuorum = quorum + } tree.GenerateDataKeyWithKeyServices(svcs) return } @@ -218,6 +224,14 @@ func main() { Name: "keyservice", Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000", }, + cli.BoolFlag{ + Name: "shamir-secret-sharing", + Usage: "use Shamir's secret sharing to split the data key among all the master keys", + }, + cli.IntFlag{ + Name: "shamir-secret-sharing-quorum", + Usage: "the number of master keys required to retrieve the data key with shamir", + }, } app.Action = func(c *cli.Context) error { @@ -600,6 +614,12 @@ func loadExample(c *cli.Context, file string, svcs []keyservice.KeyServiceClient tree.Metadata.UnencryptedSuffix = c.String("unencrypted-suffix") tree.Metadata.Version = version tree.Metadata.KeySources = ks + if c.Bool("shamir-secret-sharing") { + tree.Metadata.Shamir = true + } + if quorum := c.Int("shamir-secret-sharing-quorum"); quorum != 0 { + tree.Metadata.ShamirQuorum = quorum + } key, errs := tree.GenerateDataKeyWithKeyServices(svcs) if len(errs) > 0 { return tree, cli.NewExitError(fmt.Sprintf("Error encrypting the data key with one or more master keys: %s", errs), exitCouldNotRetrieveKey) diff --git a/functional-tests/Cargo.toml b/functional-tests/Cargo.toml index 803834fba..cdbbcfd69 100644 --- a/functional-tests/Cargo.toml +++ b/functional-tests/Cargo.toml @@ -5,7 +5,8 @@ authors = ["Adrian Utrilla "] [dependencies] tempdir = "0.3.5" -serde = "0.8" +serde = "1.0" serde_json = "0.8" serde_yaml = "0.5" +serde_derive = "1.0" lazy_static = "0.1.*" diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index 2da531ef2..252ff44bc 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -1,11 +1,15 @@ extern crate tempdir; +extern crate serde; extern crate serde_json; extern crate serde_yaml; #[macro_use] extern crate lazy_static; +#[macro_use] +extern crate serde_derive; #[cfg(test)] mod tests { + extern crate serde; extern crate serde_json; extern crate serde_yaml; @@ -15,6 +19,7 @@ mod tests { use std::process::Command; use serde_yaml::Value; const SOPS_BINARY_PATH: &'static str = "./sops"; + const SOPS_TEST_GPG_KEY: &'static str = "1022470DE3F0BC54BC6AB62DE05550BC07FB1A0A"; macro_rules! assert_encrypted { ($object:expr, $key:expr) => { @@ -267,4 +272,72 @@ sops: .success(), "SOPS failed to decrypt a file with no MAC with --ignore-mac passed in"); } + + #[test] + fn roundtrip_shamir() { + let file_path = prepare_temp_file("test_roundtrip_shamir.yaml", "a: secret".as_bytes()); + // Use the same PGP key in 10 master keys + let pgp_keys = [SOPS_TEST_GPG_KEY; 10]; + let pgp_keys = pgp_keys.join(","); + let output = Command::new(SOPS_BINARY_PATH) + .arg("--pgp") + .arg(pgp_keys) + .arg("--shamir-secret-sharing") + .arg("-i") + .arg("-e") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output.status.success(), + "SOPS failed to encrypt a file with Shamir Secret Sharing"); + let output = Command::new(SOPS_BINARY_PATH) + .arg("-d") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output + .status + .success(), + "SOPS failed to decrypt a file with Shamir Secret Sharing"); + } + + #[test] + fn roundtrip_shamir_no_quorum() { + let file_path = prepare_temp_file("test_roundtrip_shamir_no_quorum.yaml", "a: secret".as_bytes()); + // Use the same PGP key in 10 master keys + let pgp_keys = [SOPS_TEST_GPG_KEY; 10]; + let pgp_keys = pgp_keys.join(","); + let output = Command::new(SOPS_BINARY_PATH) + .arg("--pgp") + .arg(pgp_keys) + .arg("--shamir-secret-sharing") + .arg("--shamir-secret-sharing-quorum") + .arg("10") + .arg("-i") + .arg("-e") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(output.status.success(), + "SOPS failed to encrypt a file with Shamir Secret Sharing"); + // Read the output, corrupt one GPG message, and try to decrypt + let mut file = File::open(file_path.clone()).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + // This is very hacky. That string appears in the GPG messages, so we remove it to + // corrupt them + let contents = contents.replacen("wYwDEE", "", 1); + let mut file = File::create(file_path.clone()).unwrap(); + file.write_all(contents.as_bytes()).unwrap(); + drop(file); + let output = Command::new(SOPS_BINARY_PATH) + .arg("-d") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + assert!(!output + .status + .success(), + "SOPS succeeded decrypting a file with Shamir Secret Sharing without quorum"); + } } diff --git a/kms/keysource.go b/kms/keysource.go index 492b90351..50c2eeaf8 100644 --- a/kms/keysource.go +++ b/kms/keysource.go @@ -148,7 +148,7 @@ func (key MasterKey) createStsSession(config aws.Config, sess *session.Session) } func (key MasterKey) createSession() (*session.Session, error) { - re := regexp.MustCompile(`^arn:aws:kms:(.+):([0-9]+):key/(.+)$`) + re := regexp.MustCompile(`^arn:aws[\w-]*:kms:(.+):[0-9]+:key/.+$`) matches := re.FindStringSubmatch(key.Arn) if matches == nil { return nil, fmt.Errorf("No valid ARN found in %q", key.Arn) diff --git a/shamir/LICENSE b/shamir/LICENSE new file mode 100644 index 000000000..be2cc4dfb --- /dev/null +++ b/shamir/LICENSE @@ -0,0 +1,362 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. diff --git a/shamir/README.md b/shamir/README.md new file mode 100644 index 000000000..f73198baa --- /dev/null +++ b/shamir/README.md @@ -0,0 +1 @@ +Forked from [Vault](https://github.com/hashicorp/vault/tree/master/shamir) diff --git a/shamir/shamir.go b/shamir/shamir.go new file mode 100644 index 000000000..d6f5137e5 --- /dev/null +++ b/shamir/shamir.go @@ -0,0 +1,260 @@ +package shamir + +import ( + "crypto/rand" + "crypto/subtle" + "fmt" + mathrand "math/rand" + "time" +) + +const ( + // ShareOverhead is the byte size overhead of each share + // when using Split on a secret. This is caused by appending + // a one byte tag to the share. + ShareOverhead = 1 +) + +// polynomial represents a polynomial of arbitrary degree +type polynomial struct { + coefficients []uint8 +} + +// makePolynomial constructs a random polynomial of the given +// degree but with the provided intercept value. +func makePolynomial(intercept, degree uint8) (polynomial, error) { + // Create a wrapper + p := polynomial{ + coefficients: make([]byte, degree+1), + } + + // Ensure the intercept is set + p.coefficients[0] = intercept + + // Assign random co-efficients to the polynomial + if _, err := rand.Read(p.coefficients[1:]); err != nil { + return p, err + } + + return p, nil +} + +// evaluate returns the value of the polynomial for the given x +func (p *polynomial) evaluate(x uint8) uint8 { + // Special case the origin + if x == 0 { + return p.coefficients[0] + } + + // Compute the polynomial value using Horner's method. + degree := len(p.coefficients) - 1 + out := p.coefficients[degree] + for i := degree - 1; i >= 0; i-- { + coeff := p.coefficients[i] + out = add(mult(out, x), coeff) + } + return out +} + +// interpolatePolynomial takes N sample points and returns +// the value at a given x using a lagrange interpolation. +func interpolatePolynomial(x_samples, y_samples []uint8, x uint8) uint8 { + limit := len(x_samples) + var result, basis uint8 + for i := 0; i < limit; i++ { + basis = 1 + for j := 0; j < limit; j++ { + if i == j { + continue + } + num := add(x, x_samples[j]) + denom := add(x_samples[i], x_samples[j]) + term := div(num, denom) + basis = mult(basis, term) + } + group := mult(y_samples[i], basis) + result = add(result, group) + } + return result +} + +// div divides two numbers in GF(2^8) +func div(a, b uint8) uint8 { + if b == 0 { + // leaks some timing information but we don't care anyways as this + // should never happen, hence the panic + panic("divide by zero") + } + + var goodVal, zero uint8 + log_a := logTable[a] + log_b := logTable[b] + diff := (int(log_a) - int(log_b)) % 255 + if diff < 0 { + diff += 255 + } + + ret := expTable[diff] + + // Ensure we return zero if a is zero but aren't subject to timing attacks + goodVal = ret + + if subtle.ConstantTimeByteEq(a, 0) == 1 { + ret = zero + } else { + ret = goodVal + } + + return ret +} + +// mult multiplies two numbers in GF(2^8) +func mult(a, b uint8) (out uint8) { + var goodVal, zero uint8 + log_a := logTable[a] + log_b := logTable[b] + sum := (int(log_a) + int(log_b)) % 255 + + ret := expTable[sum] + + // Ensure we return zero if either a or be are zero but aren't subject to + // timing attacks + goodVal = ret + + if subtle.ConstantTimeByteEq(a, 0) == 1 { + ret = zero + } else { + ret = goodVal + } + + if subtle.ConstantTimeByteEq(b, 0) == 1 { + ret = zero + } else { + // This operation does not do anything logically useful. It + // only ensures a constant number of assignments to thwart + // timing attacks. + goodVal = zero + } + + return ret +} + +// add combines two numbers in GF(2^8) +// This can also be used for subtraction since it is symmetric. +func add(a, b uint8) uint8 { + return a ^ b +} + +// Split takes an arbitrarily long secret and generates a `parts` +// number of shares, `threshold` of which are required to reconstruct +// the secret. The parts and threshold must be at least 2, and less +// than 256. The returned shares are each one byte longer than the secret +// as they attach a tag used to reconstruct the secret. +func Split(secret []byte, parts, threshold int) ([][]byte, error) { + // Sanity check the input + if parts < threshold { + return nil, fmt.Errorf("parts cannot be less than threshold") + } + if parts > 255 { + return nil, fmt.Errorf("parts cannot exceed 255") + } + if threshold < 2 { + return nil, fmt.Errorf("threshold must be at least 2") + } + if threshold > 255 { + return nil, fmt.Errorf("threshold cannot exceed 255") + } + if len(secret) == 0 { + return nil, fmt.Errorf("cannot split an empty secret") + } + + // Generate random list of x coordinates + mathrand.Seed(time.Now().UnixNano()) + xCoordinates := mathrand.Perm(255) + + // Allocate the output array, initialize the final byte + // of the output with the offset. The representation of each + // output is {y1, y2, .., yN, x}. + out := make([][]byte, parts) + for idx := range out { + out[idx] = make([]byte, len(secret)+1) + out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1 + } + + // Construct a random polynomial for each byte of the secret. + // Because we are using a field of size 256, we can only represent + // a single byte as the intercept of the polynomial, so we must + // use a new polynomial for each byte. + for idx, val := range secret { + p, err := makePolynomial(val, uint8(threshold-1)) + if err != nil { + return nil, fmt.Errorf("failed to generate polynomial: %v", err) + } + + // Generate a `parts` number of (x,y) pairs + // We cheat by encoding the x value once as the final index, + // so that it only needs to be stored once. + for i := 0; i < parts; i++ { + x := uint8(xCoordinates[i]) + 1 + y := p.evaluate(x) + out[i][idx] = y + } + } + + // Return the encoded secrets + return out, nil +} + +// Combine is used to reverse a Split and reconstruct a secret +// once a `threshold` number of parts are available. +func Combine(parts [][]byte) ([]byte, error) { + // Verify enough parts provided + if len(parts) < 2 { + return nil, fmt.Errorf("less than two parts cannot be used to reconstruct the secret") + } + + // Verify the parts are all the same length + firstPartLen := len(parts[0]) + if firstPartLen < 2 { + return nil, fmt.Errorf("parts must be at least two bytes") + } + for i := 1; i < len(parts); i++ { + if len(parts[i]) != firstPartLen { + return nil, fmt.Errorf("all parts must be the same length") + } + } + + // Create a buffer to store the reconstructed secret + secret := make([]byte, firstPartLen-1) + + // Buffer to store the samples + x_samples := make([]uint8, len(parts)) + y_samples := make([]uint8, len(parts)) + + // Set the x value for each sample and ensure no x_sample values are the same, + // otherwise div() can be unhappy + checkMap := map[byte]bool{} + for i, part := range parts { + samp := part[firstPartLen-1] + if exists := checkMap[samp]; exists { + return nil, fmt.Errorf("duplicate part detected") + } + checkMap[samp] = true + x_samples[i] = samp + } + + // Reconstruct each byte + for idx := range secret { + // Set the y value for each sample + for i, part := range parts { + y_samples[i] = part[idx] + } + + // Interpolte the polynomial and compute the value at 0 + val := interpolatePolynomial(x_samples, y_samples, 0) + + // Evaluate the 0th value to get the intercept + secret[idx] = val + } + return secret, nil +} diff --git a/shamir/shamir_test.go b/shamir/shamir_test.go new file mode 100644 index 000000000..09f90d5fc --- /dev/null +++ b/shamir/shamir_test.go @@ -0,0 +1,198 @@ +package shamir + +import ( + "bytes" + "testing" +) + +func TestSplit_invalid(t *testing.T) { + secret := []byte("test") + + if _, err := Split(secret, 0, 0); err == nil { + t.Fatalf("expect error") + } + + if _, err := Split(secret, 2, 3); err == nil { + t.Fatalf("expect error") + } + + if _, err := Split(secret, 1000, 3); err == nil { + t.Fatalf("expect error") + } + + if _, err := Split(secret, 10, 1); err == nil { + t.Fatalf("expect error") + } + + if _, err := Split(nil, 3, 2); err == nil { + t.Fatalf("expect error") + } +} + +func TestSplit(t *testing.T) { + secret := []byte("test") + + out, err := Split(secret, 5, 3) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(out) != 5 { + t.Fatalf("bad: %v", out) + } + + for _, share := range out { + if len(share) != len(secret)+1 { + t.Fatalf("bad: %v", out) + } + } +} + +func TestCombine_invalid(t *testing.T) { + // Not enough parts + if _, err := Combine(nil); err == nil { + t.Fatalf("should err") + } + + // Mis-match in length + parts := [][]byte{ + []byte("foo"), + []byte("ba"), + } + if _, err := Combine(parts); err == nil { + t.Fatalf("should err") + } + + //Too short + parts = [][]byte{ + []byte("f"), + []byte("b"), + } + if _, err := Combine(parts); err == nil { + t.Fatalf("should err") + } + + parts = [][]byte{ + []byte("foo"), + []byte("foo"), + } + if _, err := Combine(parts); err == nil { + t.Fatalf("should err") + } +} + +func TestCombine(t *testing.T) { + secret := []byte("test") + + out, err := Split(secret, 5, 3) + if err != nil { + t.Fatalf("err: %v", err) + } + + // There is 5*4*3 possible choices, + // we will just brute force try them all + for i := 0; i < 5; i++ { + for j := 0; j < 5; j++ { + if j == i { + continue + } + for k := 0; k < 5; k++ { + if k == i || k == j { + continue + } + parts := [][]byte{out[i], out[j], out[k]} + recomb, err := Combine(parts) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !bytes.Equal(recomb, secret) { + t.Errorf("parts: (i:%d, j:%d, k:%d) %v", i, j, k, parts) + t.Fatalf("bad: %v %v", recomb, secret) + } + } + } + } +} + +func TestField_Add(t *testing.T) { + if out := add(16, 16); out != 0 { + t.Fatalf("Bad: %v 16", out) + } + + if out := add(3, 4); out != 7 { + t.Fatalf("Bad: %v 7", out) + } +} + +func TestField_Mult(t *testing.T) { + if out := mult(3, 7); out != 9 { + t.Fatalf("Bad: %v 9", out) + } + + if out := mult(3, 0); out != 0 { + t.Fatalf("Bad: %v 0", out) + } + + if out := mult(0, 3); out != 0 { + t.Fatalf("Bad: %v 0", out) + } +} + +func TestField_Divide(t *testing.T) { + if out := div(0, 7); out != 0 { + t.Fatalf("Bad: %v 0", out) + } + + if out := div(3, 3); out != 1 { + t.Fatalf("Bad: %v 1", out) + } + + if out := div(6, 3); out != 2 { + t.Fatalf("Bad: %v 2", out) + } +} + +func TestPolynomial_Random(t *testing.T) { + p, err := makePolynomial(42, 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + if p.coefficients[0] != 42 { + t.Fatalf("bad: %v", p.coefficients) + } +} + +func TestPolynomial_Eval(t *testing.T) { + p, err := makePolynomial(42, 1) + if err != nil { + t.Fatalf("err: %v", err) + } + + if out := p.evaluate(0); out != 42 { + t.Fatalf("bad: %v", out) + } + + out := p.evaluate(1) + exp := add(42, mult(1, p.coefficients[1])) + if out != exp { + t.Fatalf("bad: %v %v %v", out, exp, p.coefficients) + } +} + +func TestInterpolate_Rand(t *testing.T) { + for i := 0; i < 256; i++ { + p, err := makePolynomial(uint8(i), 2) + if err != nil { + t.Fatalf("err: %v", err) + } + + x_vals := []uint8{1, 2, 3} + y_vals := []uint8{p.evaluate(1), p.evaluate(2), p.evaluate(3)} + out := interpolatePolynomial(x_vals, y_vals, 0) + if out != uint8(i) { + t.Fatalf("Bad: %v %d", out, i) + } + } +} diff --git a/shamir/tables.go b/shamir/tables.go new file mode 100644 index 000000000..76c245e79 --- /dev/null +++ b/shamir/tables.go @@ -0,0 +1,77 @@ +package shamir + +// Tables taken from http://www.samiam.org/galois.html +// They use 0xe5 (229) as the generator + +var ( + // logTable provides the log(X)/log(g) at each index X + logTable = [256]uint8{ + 0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36, + 0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18, + 0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f, + 0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e, + 0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53, + 0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3, + 0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21, + 0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74, + 0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4, + 0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1, + 0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13, + 0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80, + 0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12, + 0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5, + 0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56, + 0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba, + 0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3, + 0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47, + 0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf, + 0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05, + 0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67, + 0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd, + 0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34, + 0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec, + 0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7, + 0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e, + 0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a, + 0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d, + 0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c, + 0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d, + 0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0, + 0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38} + + // expTable provides the anti-log or exponentiation value + // for the equivalent index + expTable = [256]uint8{ + 0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12, + 0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36, + 0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a, + 0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee, + 0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29, + 0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b, + 0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d, + 0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c, + 0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f, + 0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a, + 0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85, + 0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94, + 0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7, + 0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2, + 0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d, + 0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17, + 0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39, + 0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b, + 0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd, + 0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c, + 0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84, + 0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97, + 0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2, + 0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd, + 0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c, + 0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24, + 0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c, + 0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4, + 0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7, + 0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52, + 0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6, + 0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01} +) diff --git a/shamir/tables_test.go b/shamir/tables_test.go new file mode 100644 index 000000000..81aa983b1 --- /dev/null +++ b/shamir/tables_test.go @@ -0,0 +1,13 @@ +package shamir + +import "testing" + +func TestTables(t *testing.T) { + for i := 1; i < 256; i++ { + logV := logTable[i] + expV := expTable[logV] + if expV != uint8(i) { + t.Fatalf("bad: %d log: %d exp: %d", i, logV, expV) + } + } +} diff --git a/shamir_test.go b/shamir_test.go new file mode 100644 index 000000000..73f8752be --- /dev/null +++ b/shamir_test.go @@ -0,0 +1,135 @@ +package sops + +import ( + "go.mozilla.org/sops/keys" + "testing" + "time" + + "crypto/rand" + + "github.com/stretchr/testify/assert" +) + +type PlaintextMasterKey struct { + Key []byte +} + +func (k *PlaintextMasterKey) Encrypt(dataKey []byte) error { + k.Key = dataKey + return nil +} + +func (k *PlaintextMasterKey) EncryptIfNeeded(dataKey []byte) error { + k.Key = dataKey + return nil +} + +func (k *PlaintextMasterKey) Decrypt() ([]byte, error) { + return k.Key, nil +} + +func (k *PlaintextMasterKey) NeedsRotation() bool { + return false +} + +func (k *PlaintextMasterKey) EncryptedDataKey() []byte { + return k.Key +} + +func (k *PlaintextMasterKey) SetEncryptedDataKey(key []byte) { + k.Key = key +} + +func (k *PlaintextMasterKey) ToString() string { + return string(k.Key) +} + +func (k *PlaintextMasterKey) ToMap() map[string]interface{} { + return map[string]interface{}{ + "key": k.Key, + } +} + +func TestShamirRoundtripAllKeysAvailable(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []keys.MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + dataKey, err := m.GetDataKey() + assert.NoError(t, err) + assert.Equal(t, key, dataKey) +} + +func TestShamirRoundtripQuorumAvailable(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []keys.MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + m.KeySources[0].Keys = m.KeySources[0].Keys[:3] + dataKey, err := m.GetDataKey() + assert.NoError(t, err) + assert.Equal(t, key, dataKey) +} + +func TestShamirRoundtripNotEnoughKeys(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + ShamirQuorum: 4, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []keys.MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + m.KeySources[0].Keys = m.KeySources[0].Keys[:2] + dataKey, err := m.GetDataKey() + assert.Error(t, err) + assert.NotEqual(t, key, dataKey) +} diff --git a/sops.go b/sops.go index f430ce084..5e80b31f6 100644 --- a/sops.go +++ b/sops.go @@ -47,6 +47,7 @@ import ( "go.mozilla.org/sops/keyservice" "go.mozilla.org/sops/kms" "go.mozilla.org/sops/pgp" + "go.mozilla.org/sops/shamir" "golang.org/x/net/context" ) @@ -55,7 +56,9 @@ const DefaultUnencryptedSuffix = "_unencrypted" type sopsError string -func (e sopsError) Error() string { return string(e) } +func (e sopsError) Error() string { + return string(e) +} // MacMismatch occurs when the computed MAC does not match the expected ones const MacMismatch = sopsError("MAC mismatch") @@ -301,6 +304,12 @@ type Metadata struct { MessageAuthenticationCode string Version string KeySources []KeySource + // Shamir is true when the data key is split across multiple master keys + // according to shamir's secret sharing algorithm + Shamir bool + // ShamirQuorum is the number of master keys required to recover the + // original data key + ShamirQuorum int } // KeySource is a collection of MasterKeys with a Name. @@ -348,6 +357,11 @@ func (m *Metadata) RemoveMasterKeys(masterKeys []keys.MasterKey) { } func (m *Metadata) UpdateMasterKeysIfNeededWithKeyServices(dataKey []byte, svcs []keyservice.KeyServiceClient) (errs []error) { + // If we're using Shamir and we've added or removed keys, we must + // generate Shamir parts again and reencrypt with all keys + if m.Shamir { + return m.updateMasterKeysShamir(dataKey, svcs) + } for _, ks := range m.KeySources { for _, key := range ks.Keys { svcKey := keyservice.KeyFromMasterKey(key) @@ -383,6 +397,9 @@ func (m *Metadata) UpdateMasterKeysWithKeyServices(dataKey []byte, svcs []keyser fmt.Errorf("No key services provided, can not update master keys."), } } + if m.Shamir { + return m.updateMasterKeysShamir(dataKey, svcs) + } for _, keysource := range m.KeySources { for _, key := range keysource.Keys { svcKey := keyservice.KeyFromMasterKey(key) @@ -404,6 +421,52 @@ func (m *Metadata) UpdateMasterKeysWithKeyServices(dataKey []byte, svcs []keyser return } +// updateMasterKeysShamir splits the data key into parts using Shamir's Secret +// Sharing algorithm and encrypts each part with a master key +func (m *Metadata) updateMasterKeysShamir(dataKey []byte, svcs []keyservice.KeyServiceClient) (errs []error) { + keyCount := 0 + for _, ks := range m.KeySources { + for range ks.Keys { + keyCount++ + } + } + // If the quorum wasn't set, default to 2 + if m.ShamirQuorum == 0 { + m.ShamirQuorum = 2 + } + parts, err := shamir.Split(dataKey, keyCount, m.ShamirQuorum) + if err != nil { + errs = append(errs, fmt.Errorf("Could not split data key into parts for Shamir: %s", err)) + return + } + if len(parts) != keyCount { + errs = append(errs, fmt.Errorf("Not enough parts obtained from Shamir. Need %d, got %d", keyCount, len(parts))) + return + } + counter := 0 + for _, ks := range m.KeySources { + for _, key := range ks.Keys { + shamirPart := parts[counter] + svcKey := keyservice.KeyFromMasterKey(key) + for _, svc := range svcs { + rsp, err := svc.Encrypt(context.Background(), &keyservice.EncryptRequest{ + Key: &svcKey, + Plaintext: shamirPart, + }) + if err != nil { + errs = append(errs, fmt.Errorf("Failed to encrypt new data key with master key %q: %v\n", key.ToString(), err)) + continue + } + key.SetEncryptedDataKey(rsp.Ciphertext) + // Only need to encrypt the key successfully with one service + break + } + counter++ + } + } + return +} + // UpdateMasterKeys encrypts the data key with all master keys func (m *Metadata) UpdateMasterKeys(dataKey []byte) (errs []error) { return m.UpdateMasterKeysWithKeyServices(dataKey, []keyservice.KeyServiceClient{ @@ -466,6 +529,8 @@ func (m *Metadata) ToMap() map[string]interface{} { out["unencrypted_suffix"] = m.UnencryptedSuffix out["mac"] = m.MessageAuthenticationCode out["version"] = m.Version + out["shamir"] = m.Shamir + out["shamir_quorum"] = m.ShamirQuorum for _, ks := range m.KeySources { var keys []map[string]interface{} for _, k := range ks.Keys { @@ -476,9 +541,31 @@ func (m *Metadata) ToMap() map[string]interface{} { return out } -// GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each -// MasterKey in the Metadata's KeySources until one of them succeeds. -func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient) ([]byte, error) { +func (m Metadata) getDataKeyShamir() ([]byte, error) { + var parts [][]byte + for _, ks := range m.KeySources { + for _, k := range ks.Keys { + key, err := k.Decrypt() + if err != nil { + fmt.Printf("Key error: %s %s\n", k.ToString(), err) + } else { + parts = append(parts, key) + } + } + } + if len(parts) < m.ShamirQuorum { + return nil, fmt.Errorf("Not enough parts to recover data key with Shamir. Need %d, have %d.", m.ShamirQuorum, len(parts)) + } + dataKey, err := shamir.Combine(parts) + if err != nil { + return nil, fmt.Errorf("Could not get data key from shamir parts: %s", err) + } + return dataKey, nil +} + +// getFirstDataKey retrieves the data key from the first MasterKey in the +// Metadata's KeySources that's able to return it. +func (m Metadata) getFirstDataKey(svcs []keyservice.KeyServiceClient) ([]byte, error) { errMsg := "Could not decrypt the data key with any of the master keys:\n" for _, keysource := range m.KeySources { for _, key := range keysource.Keys { @@ -509,6 +596,16 @@ func (m Metadata) GetDataKey() ([]byte, error) { }) } +// GetDataKeyWithKeyServices retrieves the data key, asking KeyServices to decrypt it with each +// MasterKey in the Metadata's KeySources until one of them succeeds. +func (m Metadata) GetDataKeyWithKeyServices(svcs []keyservice.KeyServiceClient) ([]byte, error) { + if m.Shamir { + return m.getDataKeyShamir() + } else { + return m.getFirstDataKey(svcs) + } +} + // ToBytes converts a string, int, float or bool to a byte representation. func ToBytes(in interface{}) ([]byte, error) { switch in := in.(type) { @@ -549,6 +646,15 @@ func MapToMetadata(data map[string]interface{}) (Metadata, error) { if metadata.Version, ok = data["version"].(string); !ok { metadata.Version = strconv.FormatFloat(data["version"].(float64), 'f', -1, 64) } + shamir, ok := data["shamir"].(bool) + if ok { + metadata.Shamir = shamir + } + if shamirQuorum, ok := data["shamir_quorum"].(float64); ok { + metadata.ShamirQuorum = int(shamirQuorum) + } else if shamirQuorum, ok := data["shamir_quorum"].(int); ok { + metadata.ShamirQuorum = shamirQuorum + } if k, ok := data["kms"].([]interface{}); ok { ks, err := mapKMSEntriesToKeySource(k) if err == nil {