diff options
| author | Florian Dold <florian@dold.me> | 2021-04-13 17:17:43 +0200 | 
|---|---|---|
| committer | Florian Dold <florian@dold.me> | 2021-04-13 17:17:43 +0200 | 
| commit | 94f251fc39c71edf3c91d15e975d8f62fbd4a13d (patch) | |
| tree | 4ab196443b99763bef622780d893d96a3fb78341 /packages/taler-wallet-core/src | |
| parent | e719f7981e2b348986e03ef8a44f8a72ced5dd80 (diff) | |
add validation for forgettable JSON
Diffstat (limited to 'packages/taler-wallet-core/src')
| -rw-r--r-- | packages/taler-wallet-core/src/util/contractTerms.test.ts | 8 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/util/contractTerms.ts | 127 | 
2 files changed, 124 insertions, 11 deletions
| diff --git a/packages/taler-wallet-core/src/util/contractTerms.test.ts b/packages/taler-wallet-core/src/util/contractTerms.test.ts index afead31d0..b7783f077 100644 --- a/packages/taler-wallet-core/src/util/contractTerms.test.ts +++ b/packages/taler-wallet-core/src/util/contractTerms.test.ts @@ -86,4 +86,12 @@ test("contract terms canon hashing (nested)", (t) => {    t.is(h1, h2);    t.is(h1, h3);    t.is(h1, h4); + +  // Doesn't contain salt +  t.false(ContractTermsUtil.validateForgettable(cReq)); + +  t.true(ContractTermsUtil.validateForgettable(c1)); +  t.true(ContractTermsUtil.validateForgettable(c2)); +  t.true(ContractTermsUtil.validateForgettable(c3)); +  t.true(ContractTermsUtil.validateForgettable(c4));  }); diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts b/packages/taler-wallet-core/src/util/contractTerms.ts index 6d54f9e00..78fc8f19b 100644 --- a/packages/taler-wallet-core/src/util/contractTerms.ts +++ b/packages/taler-wallet-core/src/util/contractTerms.ts @@ -17,7 +17,6 @@  import { canonicalJson } from "@gnu-taler/taler-util";  import { kdf } from "../crypto/primitives/kdf.js";  import { -  bytesToString,    decodeCrock,    encodeCrock,    getRandomBytes, @@ -53,23 +52,28 @@ export namespace ContractTermsUtil {        for (let i = 0; i < dup.length; i++) {          dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);        } -    } else if (typeof dup === "object") { -      const fge = dup.$forgettable; -      const fgo = dup.$forgettable; -      if (typeof fge === "object") { -        for (const x of Object.keys(fge)) { +    } else if (typeof dup === "object" && dup != null) { +      if (typeof dup.$forgettable === "object") { +        for (const x of Object.keys(dup.$forgettable)) {            if (!pred([...path, x])) {              continue;            } -          delete dup[x]; -          if (!fgo[x]) { +          if (!dup.$forgotten) { +            dup.$forgotten = {}; +          } +          if (!dup.$forgotten[x]) {              const membValCanon = stringToBytes(                canonicalJson(scrub(dup[x])) + "\0",              ); -            const membSalt = decodeCrock(fge[x]); +            const membSalt = decodeCrock(dup.$forgettable[x]);              const h = kdf(64, membValCanon, membSalt, new Uint8Array([])); -            fgo[x] = encodeCrock(h); +            dup.$forgotten[x] = encodeCrock(h);            } +          delete dup[x]; +          delete dup.$forgettable[x]; +        } +        if (Object.keys(dup.$forgettable).length === 0) { +          delete dup.$forgettable;          }        }        for (const x of Object.keys(dup)) { @@ -92,7 +96,7 @@ export namespace ContractTermsUtil {        for (let i = 0; i < dup.length; i++) {          dup[i] = saltForgettable(dup[i]);        } -    } else if (typeof dup === "object") { +    } else if (typeof dup === "object" && dup !== null) {        if (typeof dup.$forgettable === "object") {          for (const k of Object.keys(dup.$forgettable)) {            if (dup.$forgettable[k] === true) { @@ -110,6 +114,107 @@ export namespace ContractTermsUtil {      return dup;    } +  const nameRegex = /^[0-9A-Za-z_]+$/; + +  /** +   * Check that the given JSON object is well-formed with regards +   * to forgettable fields and other restrictions for forgettable JSON. +   */ +  export function validateForgettable(anyJson: any): boolean { +    console.warn("calling validateForgettable", anyJson); +    if (typeof anyJson === "string") { +      return true; +    } +    if (typeof anyJson === "number") { +      return ( +        Number.isInteger(anyJson) && +        anyJson >= Number.MIN_SAFE_INTEGER && +        anyJson <= Number.MAX_SAFE_INTEGER +      ); +    } +    if (typeof anyJson === "boolean") { +      return true; +    } +    if (anyJson === null) { +      return true; +    } +    if (Array.isArray(anyJson)) { +      return anyJson.every((x) => validateForgettable(x)); +    } +    if (typeof anyJson === "object") { +      for (const k of Object.keys(anyJson)) { +        if (k.match(nameRegex)) { +          if (validateForgettable(anyJson[k])) { +            continue; +          } else { +            return false; +          } +        } +        if (k === "$forgettable") { +          const fga = anyJson.$forgettable; +          if (!fga || typeof fga !== "object") { +            return false; +          } +          for (const fk of Object.keys(fga)) { +            if (!fk.match(nameRegex)) { +              return false; +            } +            if (!(fk in anyJson)) { +              return false; +            } +            const fv = anyJson.$forgettable[fk]; +            if (typeof fv !== "string") { +              return false; +            } +            try { +              const decFv = decodeCrock(fv); +              if (decFv.length != 32) { +                return false; +              } +            } catch (e) { +              return false; +            } +          } +        } else if (k === "$forgotten") { +          const fgo = anyJson.$forgotten; +          if (!fgo || typeof fgo !== "object") { +            return false; +          } +          for (const fk of Object.keys(fgo)) { +            if (!fk.match(nameRegex)) { +              return false; +            } +            // Check that the value has actually been forgotten. +            if (fk in anyJson) { +              return false; +            } +            const fv = anyJson.$forgotten[fk]; +            if (typeof fv !== "string") { +              return false; +            } +            try { +              const decFv = decodeCrock(fv); +              if (decFv.length != 64) { +                return false; +              } +            } catch (e) { +              return false; +            } +            // Check that salt has been deleted after forgetting. +            if (anyJson.$forgettable?.[k] !== undefined) { +              return false; +            } +          } +        } else { +          console.warn("invalid type"); +          return false; +        } +      } +      return true; +    } +    return false; +  } +    /**     * Hash a contract terms object.  Forgettable fields     * are scrubbed and JSON canonicalization is applied | 
