3737
3838import com .google .api .client .util .Clock ;
3939import com .google .auth .Credentials ;
40+ import com .google .auth .credentialaccessboundary .protobuf .ClientSideAccessBoundaryProto .ClientSideAccessBoundary ;
41+ import com .google .auth .credentialaccessboundary .protobuf .ClientSideAccessBoundaryProto .ClientSideAccessBoundaryRule ;
4042import com .google .auth .http .HttpTransportFactory ;
4143import com .google .auth .oauth2 .AccessToken ;
4244import com .google .auth .oauth2 .CredentialAccessBoundary ;
45+ import com .google .auth .oauth2 .CredentialAccessBoundary .AccessBoundaryRule ;
4346import com .google .auth .oauth2 .GoogleCredentials ;
4447import com .google .auth .oauth2 .OAuth2Utils ;
4548import com .google .auth .oauth2 .StsRequestHandler ;
5356import com .google .common .util .concurrent .ListenableFuture ;
5457import com .google .common .util .concurrent .ListenableFutureTask ;
5558import com .google .common .util .concurrent .MoreExecutors ;
59+ import com .google .crypto .tink .Aead ;
60+ import com .google .crypto .tink .InsecureSecretKeyAccess ;
61+ import com .google .crypto .tink .KeysetHandle ;
62+ import com .google .crypto .tink .RegistryConfiguration ;
63+ import com .google .crypto .tink .TinkProtoKeysetFormat ;
64+ import com .google .crypto .tink .aead .AeadConfig ;
5665import com .google .errorprone .annotations .CanIgnoreReturnValue ;
66+ import dev .cel .common .CelAbstractSyntaxTree ;
67+ import dev .cel .common .CelOptions ;
68+ import dev .cel .common .CelProtoAbstractSyntaxTree ;
69+ import dev .cel .common .CelValidationException ;
70+ import dev .cel .compiler .CelCompiler ;
71+ import dev .cel .compiler .CelCompilerFactory ;
72+ import dev .cel .expr .Expr ;
5773import java .io .IOException ;
74+ import java .security .GeneralSecurityException ;
5875import java .time .Duration ;
76+ import java .util .Base64 ;
5977import java .util .Date ;
78+ import java .util .List ;
6079import java .util .concurrent .ExecutionException ;
6180import javax .annotation .Nullable ;
6281
@@ -72,6 +91,7 @@ public class ClientSideCredentialAccessBoundaryFactory {
7291 private final Object refreshLock = new byte [0 ];
7392 private volatile IntermediateCredentials intermediateCredentials = null ;
7493 private final Clock clock ;
94+ private final CelCompiler celCompiler ;
7595
7696 enum RefreshType {
7797 NONE ,
@@ -83,6 +103,18 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
83103 this .transportFactory = builder .transportFactory ;
84104 this .sourceCredential = builder .sourceCredential ;
85105 this .tokenExchangeEndpoint = builder .tokenExchangeEndpoint ;
106+
107+ // Initializes the Tink AEAD registry for encrypting the client-side
108+ // restrictions.
109+ try {
110+ AeadConfig .register ();
111+ } catch (GeneralSecurityException e ) {
112+ throw new IllegalStateException ("Error occurred when registering Tink" , e );
113+ }
114+
115+ CelOptions options = CelOptions .current ().build ();
116+ this .celCompiler = CelCompilerFactory .standardCelCompilerBuilder ().setOptions (options ).build ();
117+
86118 this .refreshMargin =
87119 builder .refreshMargin != null ? builder .refreshMargin : DEFAULT_REFRESH_MARGIN ;
88120 this .minimumTokenLifetime =
@@ -92,10 +124,37 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
92124 this .clock = builder .clock ;
93125 }
94126
95- public AccessToken generateToken (CredentialAccessBoundary accessBoundary ) {
96- // TODO(negarb/jiahuah): Implement generateToken
97- // Note: This method will call refreshCredentialsIfRequired().
98- throw new UnsupportedOperationException ("generateToken is not yet implemented." );
127+ /**
128+ * Generates a Client-Side CAB token given the {@link CredentialAccessBoundary}.
129+ *
130+ * @param accessBoundary
131+ * @return The Client-Side CAB token in an {@link AccessToken} object
132+ * @throws IOException If an I/O error occurs while refreshing the source credentials
133+ * @throws CelValidationException If the availability condition is an invalid CEL expression
134+ * @throws GeneralSecurityException If an error occurs during encryption
135+ */
136+ public AccessToken generateToken (CredentialAccessBoundary accessBoundary )
137+ throws IOException , CelValidationException , GeneralSecurityException {
138+ this .refreshCredentialsIfRequired ();
139+
140+ String intermediateToken , sessionKey ;
141+ Date intermediateTokenExpirationTime ;
142+
143+ synchronized (refreshLock ) {
144+ intermediateToken = this .intermediateCredentials .intermediateAccessToken .getTokenValue ();
145+ intermediateTokenExpirationTime =
146+ this .intermediateCredentials .intermediateAccessToken .getExpirationTime ();
147+ sessionKey = this .intermediateCredentials .accessBoundarySessionKey ;
148+ }
149+
150+ byte [] rawRestrictions = this .serializeCredentialAccessBoundary (accessBoundary );
151+
152+ byte [] encryptedRestrictions = this .encryptRestrictions (rawRestrictions , sessionKey );
153+
154+ String tokenValue =
155+ intermediateToken + "." + Base64 .getUrlEncoder ().encodeToString (encryptedRestrictions );
156+
157+ return new AccessToken (tokenValue , intermediateTokenExpirationTime );
99158 }
100159
101160 /**
@@ -403,6 +462,64 @@ public void run() {
403462 }
404463 }
405464
465+ /** Serializes a {@link CredentialAccessBoundary} object into Protobuf wire format. */
466+ @ VisibleForTesting
467+ byte [] serializeCredentialAccessBoundary (CredentialAccessBoundary credentialAccessBoundary )
468+ throws CelValidationException {
469+ List <AccessBoundaryRule > rules = credentialAccessBoundary .getAccessBoundaryRules ();
470+ ClientSideAccessBoundary .Builder accessBoundaryBuilder = ClientSideAccessBoundary .newBuilder ();
471+
472+ for (AccessBoundaryRule rule : rules ) {
473+ ClientSideAccessBoundaryRule .Builder ruleBuilder =
474+ accessBoundaryBuilder
475+ .addAccessBoundaryRulesBuilder ()
476+ .addAllAvailablePermissions (rule .getAvailablePermissions ())
477+ .setAvailableResource (rule .getAvailableResource ());
478+
479+ // Availability condition is an optional field from the CredentialAccessBoundary
480+ // CEL compliation is only performed if there is a non-empty availablity condition.
481+ if (rule .getAvailabilityCondition () != null ) {
482+ String availabilityCondition = rule .getAvailabilityCondition ().getExpression ();
483+
484+ Expr availabilityConditionExpr = this .compileCel (availabilityCondition );
485+ ruleBuilder .setCompiledAvailabilityCondition (availabilityConditionExpr );
486+ }
487+ }
488+
489+ return accessBoundaryBuilder .build ().toByteArray ();
490+ }
491+
492+ /** Compiles CEL expression from String to an {@link Expr} proto object. */
493+ private Expr compileCel (String expr ) throws CelValidationException {
494+ CelAbstractSyntaxTree ast = celCompiler .parse (expr ).getAst ();
495+
496+ CelProtoAbstractSyntaxTree astProto = CelProtoAbstractSyntaxTree .fromCelAst (ast );
497+
498+ return astProto .getExpr ();
499+ }
500+
501+ /** Encrypts the given bytes using a sessionKey using Tink Aead. */
502+ private byte [] encryptRestrictions (byte [] restriction , String sessionKey )
503+ throws GeneralSecurityException {
504+ byte [] rawKey ;
505+
506+ try {
507+ rawKey = Base64 .getDecoder ().decode (sessionKey );
508+ } catch (IllegalArgumentException e ) {
509+ // Session key from the server is expected to be Base64 encoded
510+ throw new IllegalStateException ("Session key is not Base64 encoded" , e );
511+ }
512+
513+ KeysetHandle keysetHandle =
514+ TinkProtoKeysetFormat .parseKeyset (rawKey , InsecureSecretKeyAccess .get ());
515+
516+ Aead aead = keysetHandle .getPrimitive (RegistryConfiguration .get (), Aead .class );
517+
518+ // For Client-Side CAB token encryption, empty associated data is expected.
519+ // Tink requires a byte[0] to be passed for this case.
520+ return aead .encrypt (restriction , /*associatedData=*/ new byte [0 ]);
521+ }
522+
406523 public static Builder newBuilder () {
407524 return new Builder ();
408525 }
0 commit comments