| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package io.v.android.inspectors; |
| |
| import android.util.Base64; |
| import android.util.Log; |
| |
| import com.google.common.collect.ImmutableList; |
| |
| import org.joda.time.DateTime; |
| import org.joda.time.ReadableDuration; |
| |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.interfaces.ECPublicKey; |
| import java.util.List; |
| |
| import io.v.android.v23.V; |
| import io.v.v23.context.VContext; |
| import io.v.v23.naming.Endpoint; |
| import io.v.v23.rpc.ListenSpec; |
| import io.v.v23.security.Blessings; |
| import io.v.v23.security.Caveat; |
| import io.v.v23.security.VPrincipal; |
| import io.v.v23.security.VSecurity; |
| import io.v.v23.security.access.Tag; |
| import io.v.v23.verror.VException; |
| import io.v.v23.vom.VomUtil; |
| |
| /** |
| * A RemoteInspectors object creates and manages a secure network server that allows authorized |
| * remote users to inspect the (Vanadium-specific) state of the application such as statistics, |
| * and log files. |
| * |
| * <p>When a remote user is invited via the {@link #invite(String, ReadableDuration)} method, a |
| * principal and blessing is generated that is suitable for use by the |
| * <a href="https://godoc.org/v.io/x/ref/services/debug/debug">{@code debug browse}</a> command. |
| * The blessing allows only for debug access and thus does not grant the remote user any access to |
| * change application state (techinically, does not allow them to invoke any method not tagged with |
| * {@link io.v.v23.security.access.Constants#DEBUG}). |
| */ |
| public class RemoteInspectors { |
| private static final String TAG = "RemoteInspectors"; |
| private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP; |
| private VContext mCtx; |
| private String mMountedName; |
| |
| /** |
| * Creates a secure network server to expose application state to invited remote users. |
| * |
| * @param ctx the Vanadium context of the application whose state is to be exposed |
| * @throws VException |
| */ |
| public RemoteInspectors(VContext ctx) throws VException { |
| mCtx = ctx.withCancel(); |
| try { |
| mMountedName = createMountedName(ctx); |
| } catch (NoSuchAlgorithmException e) { |
| throw new VException("Unable to create mounted name:" + e); |
| } |
| Log.i(TAG, "Application state should be inspectable via: " + mMountedName); |
| mCtx = V.withNewServer( |
| V.withListenSpec(mCtx, new ListenSpec("tcp", ":0").withProxy("proxy")), |
| mMountedName, |
| new InspectedServer() {}, |
| VSecurity.newDefaultAuthorizer()); |
| } |
| |
| /** |
| * Invite a remote user to inspect state of the application. |
| * |
| * @param invitee the name to refer to the remote user as (typically an email address) |
| * @param duration time after which inspection privileges expire |
| * |
| * @return A textual description of how the remote user can access state of the running |
| * application. |
| */ |
| public synchronized String invite(String invitee, ReadableDuration duration) |
| throws VException { |
| if (mCtx == null) { |
| throw new VException("RemoteInspectors.stop already called"); |
| } |
| |
| // In sync with the "debug delegate" command in |
| // v.io/x/ref/services/debug/debug/browse.go |
| List<Tag> tags = ImmutableList.of( |
| io.v.v23.security.access.Constants.DEBUG, |
| io.v.v23.security.access.Constants.RESOLVE); |
| |
| Caveat debugOnly = VSecurity.newCaveat( |
| io.v.v23.security.access.Constants.ACCESS_TAG_CAVEAT, tags); |
| Caveat expiration = VSecurity.newExpiryCaveat(DateTime.now().plus(duration)); |
| |
| String privateKey; |
| VPrincipal delegate; |
| try { |
| KeyPairGenerator e = KeyPairGenerator.getInstance("EC"); |
| e.initialize(256); |
| KeyPair keyPair = e.generateKeyPair(); |
| privateKey = Base64.encodeToString(keyPair.getPrivate().getEncoded(), BASE64_FLAGS); |
| delegate = VSecurity.newPrincipal( |
| VSecurity.newSigner(keyPair.getPrivate(), (ECPublicKey) keyPair.getPublic())); |
| } catch (NoSuchAlgorithmException e) { |
| throw new VException("Could not mint private key: " + e.getMessage()); |
| } |
| VPrincipal me = V.getPrincipal(mCtx); |
| Blessings b = me.bless( |
| delegate.publicKey(), |
| me.blessingStore().defaultBlessings(), |
| "debugger" + io.v.v23.security.Constants.CHAIN_SEPARATOR + invitee, |
| debugOnly, |
| expiration); |
| StringBuilder builder = new StringBuilder() |
| .append("Please inspect my application using:") |
| .append('\n') |
| .append('\n') |
| .append("debug browse") |
| .append(" --key=").append(privateKey) |
| .append(" --blessings=").append(Base64.encodeToString( |
| VomUtil.encode(b, b.getClass()), BASE64_FLAGS)) |
| .append(" "); |
| if (mMountedName.length() > 0) { |
| builder.append(mMountedName).append(" "); |
| } |
| for (Endpoint ep : V.getServer(mCtx).getStatus().getEndpoints()) { |
| builder.append(ep.name()).append(" "); |
| } |
| return builder.toString(); |
| } |
| |
| private static String createMountedName(VContext ctx) throws NoSuchAlgorithmException { |
| // TODO(ashankar): Use a conventions library here, something like |
| // https://godoc.org/v.io/v23/conventions |
| // What follows below is NOT worthy of any form of emulation! |
| // |
| // The intent is to create a unique name for this application that it will have |
| // permission to mount under. The place it has permissions to mount under depends on |
| // the kind of blessing: <idp>:u:<email>:... should have permissions under user/<email>/... |
| // while <idp>:o:<appid>:<email> should ultimately go under apps/<appid>/users/<email>/... |
| // but for now will be placed under tmp/debug/<something>. |
| // |
| // The hash of the public key of the principal is used to uniquely identify this particular |
| // instance of the application. |
| VPrincipal p = V.getPrincipal(ctx); |
| String[] myNames = VSecurity.getBlessingNames(p, p.blessingStore().defaultBlessings()); |
| String uid = Base64.encodeToString( |
| MessageDigest.getInstance("SHA-256").digest(p.publicKey().getEncoded()), |
| BASE64_FLAGS); |
| for (String n : myNames) { |
| // Does this follow the "idp:u:<username>" path? |
| String[] parts = n.split(io.v.v23.security.Constants.CHAIN_SEPARATOR); |
| if (parts.length >=3 && parts[1].equals("u")) { |
| return "users/" + parts[2] + "debug/" + uid; |
| } |
| if (parts.length >=4 && parts[1].equals("o")) { |
| // TODO(ashankar,ribrdb): Replace this with whatever convention we end up with |
| // when it comes to application blessings. |
| return "tmp/debug/" + uid; |
| } |
| } |
| return ""; |
| } |
| } |