Skip to main content
  1. Posts/

Reverse-Engineering the MapR Ticket format

··2834 words
Disclaimer. The information provided in this post is based on publicly available artifacts and information. No proprietary information is disclosed. The code snippets included have been heavily redacted for educational purposes.

Introduction #

MapR, now rebranded as HPE Ezmeral Data Fabric, is a comprehensive data platform that seamlessly integrates storage, computation, and analytics into a unified system. Built on the Hadoop stack, it offers a distributed file system, a NoSQL database, a streaming engine, and a plethora of other components.

The complexity of the MapR stack is due to its diverse components, one of which is MapR-FS, a distributed file system inspired by the Hadoop Distributed File System. Notably, MapR-FS can serve as a storage backend for Kubernetes clusters, offering a persistent storage solution for stateful applications. This is achieved through the MapR Container Storage Interface (CSI) Driver, a standard-compliant implementation of the Container Storage Interface specification. The CSI driver has its own GitHub repository but it’s not open source - in fact, there is no source code available at all, just a bunch of prebuilt container images and YAML files.

Accessing the MapR-FS storage backend necessitates a MapR ticket, a requirement that extends to Kubernetes clusters utilizing MapR-FS as a storage backend as well. These tickets can be generated by executing the maprlogin command on a machine with the MapR client installed and configured. A MapR ticket is essentially a binary, Base64 encoded blob containing authentication information for the MapR cluster with a limited lifespan, requiring regular renewal to maintain.

The Challenge #

For use with Kubernetes, the MapR ticket is stored in a Kubernetes Secret object and referenced by the PersistentVolume object. Once the ticket expires, the PersistentVolume object is no longer usable until the ticket in the Secret is renewed. An example of such a Secret and PersistentVolume object follows.

apiVersion: v1
kind: Secret
metadata:
  name: mapr-ticket-example
  namespace: default
type: Opaque
data:
  CONTAINER_TICKET: ZGVtby5tYXByLmNvbSArQ3plK3F3WUNiQVhHYno1Nk9PN1VGK2xHcUwzV1BYck5rTzFTTGF3RUVEbVNiZ05sMDE5eEJlQlkza3ZoK1IxM2l6L21DbndwenNMUXc0WTVqRW52NUd0dUlXYmVvQzk1aGE4Vkt3WDhNS2NFNktuOW5aMkFGMFFtaW5rSHdOVkJ4NlREcmlHWmZmeUpDZlp6aXZCd0JTZEtvUUVXaEJPUEZDSU1BaTd3MnpWL1NYNVV0N3U0cUlLdkVwcjBKSFY3c0xNV1lMaFluY002Q0tNZDdpRUNHdkVDc0J2RVpSVmorZHBiRVkwQmFSTi9XNTQvN3dOV2FTVkVMVUY2SldIUThkbXNxdHk0Y1psSTAvTVYxMEhaeklibDlzTUxGUT0=
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mapr-pv-example
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  capacity:
    storage: 5Gi
  csi:
    nodePublishSecretRef:
      name: "mapr-ticket-example"
      namespace: "default"
    driver: com.mapr.csi-kdf
    volumeHandle: mapr-pv-example
    volumeAttributes:
      volumePath: "/"
      cluster: "demo.mapr.com"
      cldbHosts: "10.10.102.96"
      securityType: "secure"

Management of these tickets quickly becomes a hassle, especially with a large number of access credentials to manage.

Oh, did I mention that the default maximum lifespan of a MapR ticket is 30 days? Yeah, that’s right. You quickly end up with a lot of tickets to manage and losing track of them is easy, effectively breaking whatever workload is using the PersistentVolume object.

Alright, the MapR CSI driver is closed source, does not provide any helpful information on the used tickets, and the only way to notice that a ticket has expired is when a workload using the PersistentVolume object fails and Kubernetes Events like the following starts showing up in your workload’s namespace.

Failed to start fuse process. Check cluster name and user ticket(if secure) specified

Not that great, is it? Well, time to get our hands dirty and figure out how to decode the MapR ticket format.

Looking for clues #

The ticket format is not documented anywhere, so we need to find out how it’s encoded and what information it contains. The first step is to check the maprlogin command, which is part of the MapR client and can be used to generate and renew MapR tickets. The maprlogin command has a print subcommand that prints non-sensitive information about the ticket to the console, especially the user id and the expiration date. The output looks like the following.

$ maprlogin print -ticketfile /tmp/maprticket_1000
Opening keyfile /tmp/maprticket_1000
my.cluster.com: user = juser, created = 'Mon Sep 17 08:30:26 PDT 2018', expires = 'Mon Oct 01 08:30:26 PDT 2018', RenewalTill = 'Wed Oct 17 08:30:26 PDT 2018', uid = 20001, gids = 54261, CanImpersonate = false

Playing around with the maprlogin command, one quickly notices that it’s a simple shell script wrapping some Java classes - the actual code that does the work. The relevant part of the script is the following.

"$JAVA_HOME"/bin/java ${MAPR_COMMON_JAVA_OPTS} ${MAPRLOGIN_SUPPORT_OPTS} \
    -classpath ${MAPRLOGIN_CLASSPATH}\
    ${MAPRLOGIN_OPTS} com.mapr.login.MapRLogin $args

The entry point of the maprlogin command is the com.mapr.login.MapRLogin class, which is part of one of the jar files listed in the environment variable $MAPRLOGIN_CLASSPATH. To figure out which one, we can run the following command based on this StackOverflow answer:

$ find . -name '*.jar' -print0 | \
$   xargs -0 -I '{}' sh -c 'jar tf {} | grep com.mapr.login.MapRLogin && echo {}'
com/mapr/login/MapRLogin.class
com/mapr/login/MapRLoginException.class
./maprfs-7.5.0.0-mapr.jar

Right, we found the jar file containing the MapRLogin class. All of the previously described steps require an installation of the MapR client - not exactly ideal for a quick proof of concept.

Luckily, these files can also be found on a publicly accessible Maven repository provided by HPE at repository.mapr.com, so we can download them for further analysis, e.g. v7.5.0.0 of the jar file.

Decompiling maprfs-7.5.0.0-mapr.jar #

We’ve got the jar file, now we need to figure out if we can obtain some information on the ticket format from it. The first step is to decompile the jar file to obtain the source code. Looking for modern Java decompilers, one quickly finds Procyon. Installation is as simple as running a single command with Homebrew.

$ brew install procyon-decompiler
==> Downloading https://ghcr.io/v2/homebrew/core/procyon-decompiler/manifests/0.6.0
==> Fetching procyon-decompiler
==> Downloading https://ghcr.io/v2/homebrew/core/procyon-decompiler/blobs/sha256:ee55d23c048aa221e0f2c76ea
==> Pouring procyon-decompiler--0.6.0.all.bottle.tar.gz
🍺  /opt/homebrew/Cellar/procyon-decompiler/0.6.0: 4 files, 1.9MB

Once installed, decompile the maprfs-7.5.0.0-mapr.jar file with the following command. It will take a few minutes to complete.

$ procyon-decompiler -jar ./maprfs-7.5.0.0-mapr.jar -o mapr
Decompiling com/mapr/baseutils/BaseUtilsHelper...
Decompiling com/mapr/baseutils/BinaryString...
Decompiling com/mapr/baseutils/BitSetBytesHelperUtils...
[...]
Decompiling com/mapr/login/MapRLogin...
[...]
Decompiling mapr/fs/Rpcheader...

Taking a look at the generated files, we quickly notice that Procyon generated perfectly readable Java code. Great news for us!

The MapRLogin class #

So, what does the maprlogin print command do? The main entry point of the MapRLogin class is the execute method which contains the following code:

if (command.equals("print")) {
    handlePrint(inTicketFile, type);
    return;
}

The handlePrint method performs some basic checks to make sure the ticket file exists and is readable, and then does some function calls to obtain the ticket information. The relevant part of the code can be simplified to the following.

final Security.TicketAndKey tk = com.mapr.security.Security.GetTicketAndKeyForCluster(sKType, cluster2, err);
if (tk != null) {
    printTicket(cluster2, tk);
}

Following along, we find the com.mapr.security.Security.GetTicketAndKeyForCluster method, which is defined in the com/mapr/security/Security.java file, but doesn’t really help us as it just calls out to another method GetTicketAndKeyForClusterInternal in JNISecurity where we only have the interface definition but not the implementation. Dead end? Not quite yet. The decompiled code also contains a getTicketAndKeyForCluster method in the com/mapr/security/client/ClientSecurity.java file, which does come with an implementation. The relevant part of the code is the following.

decryptedTicketAndKeyStream = this.decodeDataFromKeyFile(encryptedClientTicketAndKey);

The decodeDataFromKeyFile method is defined in the same file and is quite short, so here it is with some error handling removed for brevity.

private byte[] decodeDataFromKeyFile(final String encodedData) {
    final byte[] key = this.getKeyForKeyFile();
    final byte[] decryptedData = this.aesDecrypt(key, encryptedData);
    return decryptedData;
}

AES decryption? Interesting - maybe we will end up in a dead end after all. Let’s see what we can find out about the two methods getKeyForKeyFile and aesDecrypt. The first one, getKeyForKeyFile, is defined in the same file and is also quite short, so here it goes.

static ClientSecurity.KEY_SIZE_IN_BYTES = 32;

private byte[] getKeyForKeyFile() {
    final byte[] keybuf = new byte[ClientSecurity.KEY_SIZE_IN_BYTES];
    for (int i = 0; i < ClientSecurity.KEY_SIZE_IN_BYTES; ++i) {
        keybuf[i] = 65;
    }
    return keybuf;
}

Yes, you read that right. The key used to encrypt the ticket is a 32-byte array filled with the ASCII code 65 for the letter A. The key is also always the same as it’s hardcoded in the code. Honestly, no idea why even bother encrypting the ticket in the first place.

Anyway, we’ve got the key, now we also need to figure out how the data is encrypted. Following the second aesDecrypt method we find a run-of-the-mill AES decryption based on GCM mode. We’re going to skip the details here as we won’t implement the decryption algorithm ourselves anyway.

Proof of Concept - Decrypting a Ticket #

Putting it all together, we can reimplement the decryption of the ticket in Python. Keeping it short and simple, we’ll use the cryptography library to implement the AES decryption. The following code can be used to decrypt the ticket.

# file: decrypt.py
import sys
from base64 import b64decode

from cryptography.hazmat.primitives.ciphers.aead import AESGCM


def aes_decrypt(key: bytes, cipher_text: bytes) -> bytes:
    iv = cipher_text[:16]
    aesgcm = AESGCM(key)
    plain_text = aesgcm.decrypt(iv, cipher_text[16:], None)
    return plain_text

if __name__ == "__main__":
    # read ticket from stdin
    ticket = sys.stdin.read()

    # split ticket into host and secret
    host, secret = ticket.split(" ")

    # decrypt secret
    key: bytes = ("A" * 32).encode()
    cipher_text = b64decode(secret)
    decrypted_data = aes_decrypt(key, cipher_text)

    # print decrypted secret
    print(decrypted_data)

Let’s take some random MapR tickets one can find on the internet and check whether we can decrypt them. The following tickets have been used for testing functionality.

demo.mapr.com +Cze+qwYCbAXGbz56OO7UF+lGqL3WPXrNkO1SLawEEDmSbgNl019xBeBY3kvh+R13iz/mCnwpzsLQw4Y5jEnv5GtuIWbeoC95ha8VKwX8MKcE6Kn9nZ2AF0QminkHwNVBx6TDriGZffyJCfZzivBwBSdKoQEWhBOPFCIMAi7w2zV/SX5Ut7u4qIKvEpr0JHV7sLMWYLhYncM6CKMd7iECGvECsBvEZRVj+dpbEY0BaRN/W54/7wNWaSVELUF6JWHQ8dmsqty4cZlI0/MV10HZzIbl9sMLFQ=
demo.mapr.com cj1FDarNNKh7f+hL5ho1m32RzYyHPKuGIPJzE/CkUqEfcTGEP4YJuFlTsBmHuifI5LvNob/Y4xmDsrz9OxrBnhly/0g9xAs5ApZWNY8Rcab8q70IBYIbpu7xsBBTAiVRyLJkAtGFXNn104BB0AsS55GbQFUN9NAiWLzZY3/X1ITfGfDEGaYbWWTb1LGx6C0Jjgnr7TzXv1GqwiASbcUQCXOx4inguwMneYt9KhOp89smw6GBKP064DfIMHHR6lgv0XhBP6d9FVJ1QWKvcccvi2F3LReBtqA=
demo.mapr.com IGem6fUksZ1pd4iut978SKElS4ktecRsAkrl+qwPYc7xhfMg4wkwALKDmFmpc8Xvrm1L9Et0jVBoyhCWMDCjhToZ8b6FsfCn8wdCOB0MWm9CRobGv7MDsoEO2TQ5Bnh8i/VfuthKFxd3Om9iZPVCI4I1S9h4p/77Al1GzTGcfFFf1g9fq1HXftT9TEDyLdABIyATJbzv8zD10IDT8P1f8nxl7lgT/7ZhGz7N24vSz6jBxHE7oHmvHzjW22xJwt7TJgvrP21boH9HTsTPiKZOpQMZ4zFo6JA4aNVlQQ0=

Looking at the output of the first ticket, it’s hard to tell if it’s correct or not - it’s binary data after all. In the last few characters, we can see the string mapr, which really doesn’t look like a coincidence. The other tickets give similar results, so it looks like we’re on the right track.

$ python decrypt.py <<<"demo.mapr.com +Cze+qwYCbAXGbz56OO7UF+lGqL3WPXrNkO1SLawEEDmSbgNl019xBeBY3kvh+R13iz/mCnwpzsLQw4Y5jEnv5GtuIWbeoC95ha8VKwX8MKcE6Kn9nZ2AF0QminkHwNVBx6TDriGZffyJCfZzivBwBSdKoQEWhBOPFCIMAi7w2zV/SX5Ut7u4qIKvEpr0JHV7sLMWYLhYncM6CKMd7iECGvECsBvEZRVj+dpbEY0BaRN/W54/7wNWaSVELUF6JWHQ8dmsqty4cZlI0/MV10HZzIbl9sMLFQ="
b'\nm\x02\x08\x01zwP\x0crA\xa4\x1f4e\x9af\xa5\xd7\xbf\xd861}\xb8\x04<\xe0c\x05\x94\xe0,\xd9\xe8\xf3\x8e)\xd0\x8eX\xe4\xe6{>\xf5\x1dm\xc4\xb5\x1f\x0b\xc6B3\x96\x9e\xc5\xf7\xa0\x84\xb7\x17\xa6k\x18\xb7\x8d[\x12\xb0AoU\xb5\x01\xbb\x11\xc3\x99\x14[G\x08S#ru\x94\xdd\x14\x86\xa3\xa7Q\x9f\xc1\'u\xf3\x9b1\x90[\\\x04\x18&K9C\x12"\n \xb7\x89wq!N\x0e\xf8\xa7!&\x14\xaf\xa0\xb7\xf7\x89\xcd\xf7\r\xe0n\x0f\x98\xb2\x95x|\x8b\xbe\xbeD\x1a\x11\x08\x88\'\x10\x88\'\x10\x00\x10\x89\'"\x04mapr \xe5\x90\x8e\xeb\xc5\xdb\xd1\x01(\xc9\xc3\x93\xd6\x050\x00'

It’s still binary data, so we need to figure out how to decode it.

Protocol Buffers #

Checking the code in the com/mapr/security/client/ClientSecurity.java file again, we quickly notice imports like the following:

import com.google.protobuf.InvalidProtocolBufferException;
import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;
import com.mapr.fs.proto.Security;
...

Protobuf, or Protocol Buffers, is a language and platform-neutral data serialization format initially developed by Google. One of its many advantages is that it provides a simple textual way to define the data format in .proto files, which can then be transpiled to code in the language of your choice. The com.mapr.fs.proto.Security class is one of those generated classes. And it contains quite a lot of generated code - the decompiled file has over 16k lines of code.

What have we got here? #

Checking some questions on StackOverflow ( mainly this one) we find that there is some alternative representation descriptorProto of the original protobuf definition found in the generated code, i.e. as follows:

final String[] descriptorData = { "\n\u000esecurity.proto\u0012\u0007mapr.fs\"¬\u0002\n\u000eCredentialsMsg ... " };

Using the protobuf library and some of it’s methods, we can parse this binary data and obtain a binary representation of the original protobuf definition as a protobuf itself - still not the final .proto file we’re looking for as it can’t be used to generate code, but it’s a start.

The following code can be used to parse the descriptorData variable from above.

# file: rebuild_textproto.py
import sys
import re

import google.protobuf.descriptor_pb2 as descriptor_pb2

# Read binary data from stdin
for line in sys.stdin.buffer:
    if b"security.proto" in line:
        # we found the line we are looking for, so we can stop iterating
        break

# regex to find the string between `= { ... }`
m = re.search(r"^.*=\s*\{\s*\"(.*)\"\s*\}.*$", line.decode("utf-8"))
if m:
    data = m.group(1)
else:
    raise Exception("Could not find the string between `= { ... }`")

# fix encoding ... this is a bit hacky, but it works
data = data.encode("latin-1").decode("unicode-escape").encode("latin-1")

# create a new FileDescriptorSet
fds = descriptor_pb2.FileDescriptorSet()
fds.file.append(descriptor_pb2.FileDescriptorProto())

# convert string to bytes, serialize it to bytes, and write it out
fds.file[0].ParseFromString(data)
serialized_data = fds.file[0].SerializeToString()
sys.stdout.buffer.write(serialized_data)

Running the script on the content of the com/mapr/fs/proto/Security.java file that contains the generated com.mapr.fs.proto.Security class from above, we get the following binary output, as always truncated for brevity.

$ python rebuild_textproto.py < Security.java

security.protomapr.fs"�
CredentialsMsg

uid (

[...]

com.mapr.fs.protoHZ ezmeral.hpe.com/datafab/fs/proto

Alright, another binary blob. What can we do with this? Well … some more google-fu leads us to this question on StackOverflow telling us that the C++ protobuf library - and only the C++ one - has a DebugString() function that allows us to debug print the binary data as a proper textual protobuf definition. Let’s give it a try.

Sprinkle some C++ on it #

To use any of the C++ protobuf libraries, we first need to install them. As before, we’ll take the easy route and use Homebrew, so installation is as simple as running the following command.

$ brew install protobuf
==> Downloading https://ghcr.io/v2/homebrew/core/protobuf/manifests/25.1-1
==> Fetching protobuf
==> Downloading https://ghcr.io/v2/homebrew/core/protobuf/blobs/sha256:76050775316e70aef31157c84acd3399e3336dcfd8e7b40cd451967be3c8fbf3
==> Installing protobuf
==> Pouring protobuf--25.1.arm64_sonoma.bottle.1.tar.gz
==> Summary
🍺  /opt/homebrew/Cellar/protobuf/25.1: 402 files, 13.6MB

The code for the DebugString() workaround is the following.

// file: fds2proto.cpp
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <iostream>

int main()
{
  google::protobuf::FileDescriptorProto fileProto;

  if (!fileProto.ParseFromIstream(&std::cin))
  {
    std::cerr << "Failed to parse FileDescriptorProto from stdin" << std::endl;
    return 1;
  }

  google::protobuf::DescriptorPool pool;
  const google::protobuf::FileDescriptor* desc = pool.BuildFile(fileProto);
  std::cout << desc->DebugString() << std::endl;

  return 0;
}

We’ll just assume that you’ve got gcc / g++ installed already - to compile the above code with the protobuf library, we can run the following commands.

$ export CPATH=/opt/homebrew/include
$ export LIBRARY_PATH=/opt/homebrew/lib
$ export LD_LIBRARY_PATH=/opt/homebrew/lib:$LD_LIBRARY_PATH
$ g++ -o fds2proto fds2proto.cpp -std=c++17 -lprotobuf -pthread

Once done, we can chain the rebuild_textproto.py and fds2proto calls together to get the final protobuf definition.

$ python rebuild_textproto.py < Security.java | ./fds2proto | tee security.proto
syntax = "proto2";

package mapr.fs;

option java_package = "com.mapr.fs.proto";
option optimize_for = LITE_RUNTIME;
option go_package = "ezmeral.hpe.com/datafab/fs/proto";

enum SecurityProg {
  ChallengeResponseProc = 1;
  RefreshTicketProc = 2;
}

[...]

message GetJwtTicketResponse {
  optional string error = 1;
  optional int32 status = 2;
  optional bytes maprTicket = 3;
}

Ah, there we go. We’ve got the protobuf definition we’ve been looking for. The protobuf definition is quite long, as it also contains basically all the security-related protobuf definitions from the MapR codebase. We’re only interested in the ticket format, so we could remove all the other definitions, if we wanted to. This security.proto file can now also be used to generate parser code for other languages, e.g. Python or Go. For a quick proof of concept, we’ll go with Python.

Proof of Concept - Putting it all together #

Alright, we know how to decode and decrypt MapR tickets, we also got the protobuf definition of the ticket format. Let’s put it all together and see if this is as simple as it looks.

To generate the Python code from the obtained security.proto file, we can use the protoc command from the protobuf library we had to install earlier for the C++ code anyway.

$ protoc --proto_path=./ --pyi_out=./ --python_out=./ security.proto

This will generate the files security_pb2.py and security_pb2.pyi - the first one defining the protobuf classes and the second one providing type hints for the first one. These files can now be imported in our own code to parse the ticket data.

# file: parse.py
import sys
from base64 import b64decode

from decrypt import aes_decrypt
from security_pb2 import TicketAndKey

# read ticket from stdin
ticket = sys.stdin.read()

# split ticket into host and secret
host, secret = ticket.split(" ")

# decrypt secret
key: bytes = ("A" * 32).encode()
cipher_text = b64decode(secret)
decrypted_data = aes_decrypt(key, cipher_text)

# parse the ticket into the protobuf
ticket_and_key = TicketAndKey()
ticket_and_key.ParseFromString(decrypted_data)

# print parsed ticket
print(ticket_and_key)

Let’s check out whether this works by trying to parse the ticket data from above.

$ python parse.py <<<"demo.mapr.com +Cze+qwYCbAXGbz56OO7UF+lGqL3WPXrNkO1SLawEEDmSbgNl019xBeBY3kvh+R13iz/mCnwpzsLQw4Y5jEnv5GtuIWbeoC95ha8VKwX8MKcE6Kn9nZ2AF0QminkHwNVBx6TDriGZffyJCfZzivBwBSdKoQEWhBOPFCIMAi7w2zV/SX5Ut7u4qIKvEpr0JHV7sLMWYLhYncM6CKMd7iECGvECsBvEZRVj+dpbEY0BaRN/W54/7wNWaSVELUF6JWHQ8dmsqty4cZlI0/MV10HZzIbl9sMLFQ="
encryptedTicket: "..."
userKey {
  key: "..."
}
userCreds {
  uid: 5000
  gids: 5000
  gids: 0
  gids: 5001
  userName: "mapr"
}
expiryTime: 922337203685477
creationTimeSec: 1522852297
maxRenewalDurationSec: 0

$ python parse.py <<<"demo.mapr.com cj1FDarNNKh7f+hL5ho1m32RzYyHPKuGIPJzE/CkUqEfcTGEP4YJuFlTsBmHuifI5LvNob/Y4xmDsrz9OxrBnhly/0g9xAs5ApZWNY8Rcab8q70IBYIbpu7xsBBTAiVRyLJkAtGFXNn104BB0AsS55GbQFUN9NAiWLzZY3/X1ITfGfDEGaYbWWTb1LGx6C0Jjgnr7TzXv1GqwiASbcUQCXOx4inguwMneYt9KhOp89smw6GBKP064DfIMHHR6lgv0XhBP6d9FVJ1QWKvcccvi2F3LReBtqA="
encryptedTicket: "..."
userKey {
  key: "..."
}
userCreds {
  uid: 5000
  gids: 5000
  gids: 1000
  userName: "mapr"
}
expiryTime: 1550578429
creationTimeSec: 1549368829
maxRenewalDurationSec: 2592000
canUserImpersonate: true

$ python parse.py <<<"demo.mapr.com IGem6fUksZ1pd4iut978SKElS4ktecRsAkrl+qwPYc7xhfMg4wkwALKDmFmpc8Xvrm1L9Et0jVBoyhCWMDCjhToZ8b6FsfCn8wdCOB0MWm9CRobGv7MDsoEO2TQ5Bnh8i/VfuthKFxd3Om9iZPVCI4I1S9h4p/77Al1GzTGcfFFf1g9fq1HXftT9TEDyLdABIyATJbzv8zD10IDT8P1f8nxl7lgT/7ZhGz7N24vSz6jBxHE7oHmvHzjW22xJwt7TJgvrP21boH9HTsTPiKZOpQMZ4zFo6JA4aNVlQQ0="
encryptedTicket: "..."
userKey {
  key: "..."
}
userCreds {
  uid: 5000
  gids: 5000
  gids: 5003
  gids: 0
  userName: "mapr"
}
expiryTime: 1619735566
creationTimeSec: 1618525966
maxRenewalDurationSec: 2592000
canUserImpersonate: true
isExternal: true

Nice! We’ve got all the data that we also get from the maprlogin print command, without the need to run any components of the MapR stack or even the need to interact with any MapR cluster nodes. Great news, as it means we can now easily decode MapR tickets in our code without the need to rely on the maprlogin command.

Even better, we can use this as a base to create a simple custom Kubernetes controller watching all Secret objects with a valid MapR ticket and automatically putting metadata on the Secret object to make it easier to identify key information like the expiration date.

This will be released as a separate project in the future, so stay tuned for that.

Conclusion #

In short, all of this was mostly possible because Java - or at least the jar files as used by MapR - can be decompiled into perfectly readable code. Everything else was just a matter of following the code and figuring out how the data was encoded and encrypted. The only thing that was a bit of a hassle was the protobuf definition, but even that was possible to reconstruct with a bit of effort.

A Go module implementing the encode and decode functionality for MapR tickets is available at GitHub and GoDoc.