Giter VIP home page Giter VIP logo

nitram509 / jmacaroons Goto Github PK

View Code? Open in Web Editor NEW
115.0 7.0 11.0 1.44 MB

Pure Java implementation of Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud. Android ready. Online playground available. Project is STALE

Home Page: http://www.macaroons.io

License: Apache License 2.0

Java 100.00%
macaroons crypto crypto-library cryptography java-library authentication cryptography-library

jmacaroons's Introduction

Macaroons are Better Than Cookies!

This Java library provides an implementation of macaroons[1], which are flexible authorization tokens that work great in distributed systems. Like cookies, macaroons are bearer tokens that enable applications to ascertain whether their holders' actions are authorized. But macaroons are better than cookies!

This project started as a port of libmacaroons[2] library. The primary goals are

  • being compatible to libmacaroons
  • having no external dependencies, except the Java Runtime
  • being very much backward compatible, while using Java8
  • focus on binary serialization format (currently, JSON format isn't supported)
  • being the reference implementation in the Java community ;-)

There is a playground (testing environment) available, where you can build and verify macaroons online.

License

License

Usage/Import In Your Project

This library jmacaroons is available via Maven Central. Requires Java 1.8+

Maven Central

Maven

<dependency>
  <groupId>com.github.nitram509</groupId>
  <artifactId>jmacaroons</artifactId>
  <version>0.4.2</version>
</dependency>

Gradle

compile 'com.github.nitram509:jmacaroons:0.4.2'

Build Status

maven test

codecov

Community & Badges

Listed on Android Arsenal: Android Arsenal

Creating Your First Macaroon

Lets create a simple macaroon Of course, this macaroon can be displayed in a more human-readable form for easy debugging

  Macaroon create() {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";
    Macaroon macaroon = Macaroon.create(location, secretKey, identifier);
    System.out.println(macaroon.inspect());
    // > location http://www.example.org
    // > identifier we used our secret key
    // > signature e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f
    return macaroon;
  }

Serializing

Macaroons are serialized, using Base64 URL safe encoding RFC 4648. This way you can very easily append it to query string within URIs.

  void serialize() {
    Macaroon macaroon = create();
    String serialized = macaroon.serialize();
    System.out.println("Serialized: " + serialized);
    // Serialized: MDAyNGxvY2F0aW9uIGh0dHA6Ly93d3cuZXhhbXBsZS5vcmcKMDAyNmlkZW50aWZpZXIgd2UgdXNlZCBvdXIgc2VjcmV0IGtleQowMDJmc2lnbmF0dXJlIOPZ4CkIUmxMADmuFRFBFdl_3Wi_K6N5s0Kq8PYX0FUvCg
  }

Alternatively, the V2 binary serializer format is supported.

  void serialize_v2_binary_format() {
    Macaroon macaroon = create();
    String serialized = macaroon.serialize(V2);
    System.out.println("Serialized: " + serialized);
    // Serialized: AgEWaHR0cDovL3d3dy5leGFtcGxlLm9yZwIWd2UgdXNlZCBvdXIgc2VjcmV0IGtleQAABiDj2eApCFJsTAA5rhURQRXZf91ovyujebNCqvD2F9BVLw
  }

Note: Base64 URL safe is supported since v0.3.0. jmacaroons also de-serializes regular Base64 to maintain backward compatibility.

De-Serializing

  void deserialize() {
    String serialized = create().serialize();
    Macaroon macaroon = Macaroon.deserialize(serialized);
    System.out.println(macaroon.inspect());
    // > location http://www.example.org
    // > identifier we used our secret key
    // > signature e3d9e02908526c4c0039ae15114115d97fdd68bf2ba379b342aaf0f617d0552f
  }

Verifying Your Macaroon

A verifier can only ever successfully verify a macaroon when provided with the macaroon and its corresponding secret - no secret, no authorization.

  void verify() throws InvalidKeyException, NoSuchAlgorithmException {
    Macaroon macaroon = create();
    MacaroonsVerifier verifier = new MacaroonsVerifier(macaroon);
    String secret = "this is our super secret key; only we should know it";
    boolean valid = verifier.isValid(secret);
    // > True
  }

Adding Caveats

When creating a new macaroon, you can add a caveat to our macaroon that restricts it to just the account number 3735928559.

  void addCaveat() throws InvalidKeyException, NoSuchAlgorithmException {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";
    Macaroon macaroon = Macaroon.builder(location, secretKey, identifier)
        .addCaveat("account = 3735928559")
        .build();
    System.out.println(macaroon.inspect());
  }

Because macaroon objects are immutable, they have to be modified via Macaroon.builder(). Thus, a new macaroon object will be created.

  void addCaveat_modify() throws InvalidKeyException, NoSuchAlgorithmException {
    Macaroon macaroon = create();
    macaroon = Macaroon.builder(macaroon)
        .addCaveat("account = 3735928559")
        .build();
    System.out.println(macaroon.inspect());
    // > location http://www.example.org
    // > identifier we used our secret key
    // > cid account = 3735928559
    // > signature 1efe4763f290dbce0c1d08477367e11f4eee456a64933cf662d79772dbb82128
  }

Verifying Macaroons With Caveats

The verifier should say that this macaroon is unauthorized because the verifier cannot prove that the caveat (account = 3735928559) is satisfied. We can see that it fails just as we would expect.

  void verify_required_caveats() throws InvalidKeyException, NoSuchAlgorithmException {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";
    Macaroon macaroon = Macaroon.builder(location, secretKey, identifier)
        .addCaveat("account = 3735928559")
        .build();
    MacaroonsVerifier verifier = new MacaroonsVerifier(macaroon);
    verifier.isValid(secretKey);
    // > False

Caveats like these are called "exact caveats" because there is exactly one way to satisfy them. Either the account number is 3735928559, or it isn't. At verification time, the verifier will check each caveat in the macaroon against the list of satisfied caveats provided to "satisfyExact()". When it finds a match, it knows that the caveat holds and it can move onto the next caveat in the macaroon.

    verifier.satisfyExact("account = 3735928559");
    verifier.isValid(secretKey);
    // > True

The verifier can be made more general, and be "future-proofed", so that it will still function correctly even if somehow the authorization policy changes; for example, by adding the three following facts, the verifier will continue to work even if someone decides to self-attenuate itself macaroons to be only usable from IP address and browser:

    verifier.satisfyExact("IP = 127.0.0.1')");
    verifier.satisfyExact("browser = Chrome')");
    verifier.satisfyExact("action = deposit");
    verifier.isValid(secretKey);
    // > True
  }

There is also a more general way to check caveats, via callbacks. When providing such a callback to the verifier, it is able to check if the caveat satisfies special constrains.

  void verify_general_caveats() throws InvalidKeyException, NoSuchAlgorithmException {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";

    Macaroon macaroon = Macaroon.builder(location, secretKey, identifier)
        .addCaveat("time < 2042-01-01T00:00")
        .build();
    MacaroonsVerifier verifier = new MacaroonsVerifier(macaroon);
    verifier.isValid(secretKey);
    // > False

    verifier.satisfyGeneral(new TimestampCaveatVerifier());
    verifier.isValid(secretKey);
    // > True
  }

Third Party Caveats

Like first-party caveats, third-party caveats restrict the context in which a macaroon is authorized, but with a different form of restriction. Where a first-party caveat is checked directly within the verifier, a third-party caveat is checked by the third-party, who provides a discharge macaroon to prove that the original third-party caveat is true. The discharge macaroon is recursively inspected by the verifier; if it verifies successfully, the discharge macaroon serves as a proof that the original third-party caveat is satisfied. Of course, nothing stops discharge macaroons from containing embedded first- or third-party caveats for the verifier to consider during verification.

Let's rework the above example to provide Alice with access to her account only after she authenticates with a service that is separate from the service processing her banking transactions.

As before, we'll start by constructing a new macaroon with the caveat that is limited to Alice's bank account.

  void with_3rd_party_caveats() {
    // create a simple macaroon first
    String location = "http://mybank/";
    String secret = "this is a different super-secret key; never use the same secret twice";
    String publicIdentifier = "we used our other secret key";
    MacaroonsBuilder mb = Macaroon.builder(location, secret, publicIdentifier)
        .addCaveat("account = 3735928559");

    // add a 3rd party caveat
    // you'll likely want to use a higher entropy source to generate this key
    String caveat_key = "4; guaranteed random by a fair toss of the dice";
    String predicate = "user = Alice";
    // send_to_3rd_party_location_and_do_auth(caveat_key, predicate);
    // identifier = recv_from_auth();
    String identifier = "this was how we remind auth of key/pred";
    Macaroon m = mb.addCaveat("http://auth.mybank/", caveat_key, identifier)
        .build();

    System.out.println(m.inspect());
    // > location http://mybank/
    // > identifier we used our other secret key
    // > cid account = 3735928559
    // > cid this was how we remind auth of key/pred
    // > vid AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr
    // > cl http://auth.mybank/
    // > signature d27db2fd1f22760e4c3dae8137e2d8fc1df6c0741c18aed4b97256bf78d1f55c

In a real application, we'd look at these third party caveats, and contact each location to retrieve the requisite discharge macaroons. We would include the identifier for the caveat in the request itself, so that the server can recall the secret used to create the third-party caveat. The server can then generate and return a new macaroon that discharges the caveat:

    final String oneHourFromNow = Instant.now()
        .plus(Duration.ofHours(1))
        .toString();

    Macaroon d = Macaroon.builder("http://auth.mybank/", caveat_key, identifier)
        .addCaveat("time < " + oneHourFromNow)
        .build();

This new macaroon enables the verifier to determine that the third party caveat is satisfied. Our target service added a time-limiting caveat to this macaroon that ensures that this discharge macaroon does not last forever. This ensures that Alice (or, at least someone authenticated as Alice) cannot use the discharge macaroon indefinitely and will eventually have to re-authenticate.

Once Alice has both the root macaroon and the discharge macaroon in her possession, she can make the request to the target service. Making a request with discharge macaroons is only slightly more complicated than making requests with a single macaroon. In addition to serializing and transmitting all involved macaroons, there is preparation step that binds the discharge macaroons to the root macaroon. This binding step ensures that the discharge macaroon is useful only when presented alongside the root macaroon. The root macaroon is used to bind the discharge macaroons like this:

    Macaroon dp = Macaroon.builder(m)
        .prepareForRequest(d)
        .build();

If we were to look at the signatures on these prepared discharge macaroons, we would see that the binding process has irreversibly altered their signature(s).

    System.out.println("d.signature = " + d.signature);
    System.out.println("dp.signature = " + dp.signature);
    // > d.signature = 82a80681f9f32d419af12f6a71787a1bac3ab199df934ed950ddf20c25ac8c65
    // > dp.signature = 2eb01d0dd2b4475330739140188648cf25dda0425ea9f661f1574ca0a9eac54e

The root macaroon 'm' and its discharge macaroons 'dp' are ready for the request. Alice can serialize them all and send them to the bank to prove she is authorized to access her account. The bank can verify them using the same verifier we built before. We provide the discharge macaroons as a third argument to the verify call:

    new MacaroonsVerifier(m)
        .satisfyExact("account = 3735928559")
        .satisfyGeneral(new TimestampCaveatVerifier())
        .satisfy3rdParty(dp)
        .assertIsValid(secret);
    // > ok.
  }

Without the 'prepare_for_request()' call, the verification would fail.

Commonly used verifier, shipped with jmacaroons

Time to live verification

Applying a timestamp in the future to a macaroon will provide time to live semantics. Given that all machines have synchronized clocks, a general macaroon verifier is able to check for expiration.

  void timestamp_verifier() {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";

    Macaroon macaroon = Macaroon.builder(location, secretKey, identifier)
        .addCaveat("time < 2015-01-01T00:00")
        .build();

    new MacaroonsVerifier(macaroon)
        .satisfyGeneral(new TimestampCaveatVerifier())
        .isValid(secretKey);
    // > True
  }
Authorities verification

Macaroons may also embed authorities. Thus a general macaroon verifier is able to check for a single authority.

  void authorities_verifier() {
    String location = "http://www.example.org";
    String secretKey = "this is our super secret key; only we should know it";
    String identifier = "we used our secret key";

    Macaroon macaroon = Macaroon.builder(location, secretKey, identifier)
        .addCaveat("authorities = ROLE_USER, DEV_TOOLS_AVAILABLE")
        .build();

    new MacaroonsVerifier(macaroon)
        .satisfyGeneral(hasAuthority("DEV_TOOLS_AVAILABLE"))
        .isValid(secretKey);
    // > True
  }

Choosing Secrets

For clarity, we've generated human-readable secrets that we use as the root keys of all of our macaroons. In practice, this is terribly insecure and can lead to macaroons that can easily be forged because the secret is too predictable. To avoid this, we recommend generating secrets using a sufficient number of suitably random bytes. Because the bytes are a secret key, they should be drawn from a source with enough entropy to ensure that the key cannot be guessed before the macaroon falls out of use.

The jmacaroons library exposes a constant that is the ideal number of bytes these secret keys should contain. Any shorter is wasting an opportunity for security.

com.github.nitram509.jmacaroons.MacaroonsConstants.MACAROON_SUGGESTED_SECRET_LENGTH = 32

Performance

There's a little micro benchmark, which demonstrates the performance of jmacaroons.

Source: https://gist.github.com/nitram509/b6f836a697b405e5f440

Environment: Windows 8.1 64bit, JRE 1.8.0_25 64bit, Intel i7-4790 @3.60GHz

Results
----------
Benchmark                                                                    Mode  Samples        Score       Error  Units
o.s.JMacaroonsBenchmark.benchmark_Deserialize                               thrpt        5  2190474,677 ± 44591,197  ops/s
o.s.JMacaroonsBenchmark.benchmark_Deserialize_and_Verify_key_bytes          thrpt        5   457262,262 ±  5868,723  ops/s
o.s.JMacaroonsBenchmark.benchmark_Deserialize_and_Verify_key_string         thrpt        5   262689,398 ±  4270,857  ops/s
o.s.JMacaroonsBenchmark.benchmark_Serialize_with_key_bytes                  thrpt        5   424008,024 ± 16222,450  ops/s
o.s.JMacaroonsBenchmark.benchmark_Serialize_with_key_bytes_and_1_caveat     thrpt        5   242060,835 ±  5696,272  ops/s
o.s.JMacaroonsBenchmark.benchmark_Serialize_with_key_bytes_and_2_caveats    thrpt        5   166017,277 ±   870,467  ops/s
o.s.JMacaroonsBenchmark.benchmark_Serialize_with_key_bytes_and_3_caveats    thrpt        5   127712,773 ±   478,394  ops/s
o.s.JMacaroonsBenchmark.benchmark_Serialize_with_key_string                 thrpt        5   252302,839 ±  3277,232  ops/s

Stargazers over time

Stargazers over time

jmacaroons's People

Contributors

actions-user avatar benjumanji avatar cyberdelia avatar dependabot-preview[bot] avatar dependabot[bot] avatar jaxley avatar kofemann avatar nitram509 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

jmacaroons's Issues

Need to expose verification errors to callers

Currently, the implementation generates fairly-detailed verification errors objects based on what verification failed. But the current API keeps those entirely internal. This prevents callers from determining what caused an authorization failure.

A common use case would be that a server would need to be able to communicate to a caller that a macaroon or discharge macaroon has expired and they need to obtain a new one. That's opaque right now because .isValid and .assertValid only generate booleans.

Debugging is also a challenge since I've had to put breakpoints in the code in order to determine which failure occurred (whether expected or not). Would also help unit testability to be able to ensure that validations occur because of expected conditions and not coincidence or serendipity.

Inability to specify binary caveat keys for 3rd party breaks 3rd party caveat validation

The API lets you specify either a String as a root key or a byte array for root keys. If you specify a string, then internally it uses a key derivation algorithm to derive a key from that (presumably assuming someone using the String-based APIs is providing a low-quality password that needs to be expanded into a derived key?)

However, there is no such byte array option for third party caveats. You can only specify a key by a String, which results in a derived value that you don't control.

Was tearing my hair out trying to understand why the validation was failing until I realized that the key used as byte array in one instance results in a signature different than the one expected because the string key is changed to a derived key so they'll never match.

Unit test to demonstrate the error (requires java language level of 1.8+):

  @Test
  public void using_non_string_key_bytes_for_3rd_party__should_succeed() {
    byte[] root_key = keyGen();
    byte[] caveat_key = keyGen();
    String caveat_id = "caveat";
    String macaroon_id = "123456";
    Macaroon M = new MacaroonsBuilder("some-service", root_key, macaroon_id)
        .add_third_party_caveat("authN", new String(caveat_key), caveat_id)
        .getMacaroon();

    assertThat(new MacaroonsVerifier(M)
        .isValid(root_key))
        .describedAs("Original should not be valid without discharge macaroon")
        .isFalse();

    // create discharge macaroon using caveat key as bytes
    Macaroon D = new MacaroonsBuilder("authN", caveat_key, caveat_id)
        .getMacaroon();

    assertThat(new MacaroonsVerifier(D)
        .isValid(caveat_key))
        .describedAs("Discharge macaroon should be valid")
        .isTrue();

    // prepare discharge macaroon by binding to the original macaroon
    Macaroon bound = new MacaroonsBuilder(M)
        .prepare_for_request(D)
        .getMacaroon();

    assertThat(new MacaroonsVerifier(bound)
        .isValid(caveat_key))
        .describedAs("Bound discharge macaroon should not be considered valid on its own")
        .isFalse();

    assertThat(new MacaroonsVerifier(M)
        .satisfy3rdParty(bound)
        .isValid(root_key))
        .describedAs("Original should be valid with third party caveat bound")
        .isTrue();
  }

  byte[] keyGen() {
    try {
      byte[] keyBytes = new byte[32];
      SecureRandom prng = SecureRandom.getInstanceStrong();
      prng.nextBytes(keyBytes);
      return keyBytes;
    }
    catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return null;
  }

Verification of thirdparty caveats is optional

This isn't the default behaviour of the reference libmacaroons, doesn't line up with the discussion in the original paper, and also just seems like a disaster waiting to happen. Making the guarantees of what a macaroon means weaker should be an explicit opt-in, and truthfully, I can't think of a single instance where you would want to caveat macaroon and then have that caveat ignored at verfication time.

Any plans for V2 Macaroon support

Hi

I use your project for parsing macaroons generated from an application that just switched to V2 Macaroons. Are there any plans for this project to support this in future?

Regards
Philip Vendil

Add a caveat extractor system?

In many projects, there are needs of data extraction from the caveat, and at the moment, extraction has to be made manually iteration on top of macaroon after the validation. This issue is made to discuss if we need to add a value extractor to caveat management in jmacaroons.

I quickly did something on a project, it a crypto token translation system, from macaroons to warp10 tokens, it's far from perfect, but it can be a good starting point https://github.com/CleverCloud/warp10-macaroons-plugin

Do you think I have to explore the possibility and make a pull request?

Proof of life

Looks like this library may be the only available Java macaroon implementation? But it's not actively maintained it seems. Any plans to keep this going and maintain compatibility with the other implementations across languages?

`SimpleDateFormat` is not threadsafe

Browsing through the code I found the following in TimestampCaveatVerifier:

private static SimpleDateFormat ISO_DateFormat_DAY = new SimpleDateFormat("yyyy-MM-dd");
private static SimpleDateFormat ISO_DateFormat_HOUR = new SimpleDateFormat("yyyy-MM-dd'T'HH");
private static SimpleDateFormat ISO_DateFormat_MINUTE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
private static SimpleDateFormat ISO_DateFormat_SECOND = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private static SimpleDateFormat ISO_DateFormat_TIMEZONE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

As these variables are static, they are shared between all TimestampCaveatVerifier instances. Therefore use of the TimestampCaveatVerifier in a multithreaded environment is impossible, unless one shares a single instance and synchronises it.

Consider making the SimpleDateFormat instances non-static. If performance is a concern then a BlockingQueue can be used as a pool.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.