modules/kms-keyring/src/arn_parsing.ts (141 lines of code) (raw):

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { needs } from '@aws-crypto/material-management' /* See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms * regex to match: 'resourceType/resourceId' || 'resourceType' * This is complicated because the `split(':')`. * The valid resourceType resourceId delimiters are `/`, `:`. * This means if the delimiter is a `:` it will be split out, * when splitting the whole arn. */ export const KMS_SERVICE = 'kms' export type ParsedAwsKmsKeyArn = { Partition: string Region: string AccountId: string ResourceType: string ResourceId: string } const ARN_PREFIX = 'arn' const KEY_RESOURCE_TYPE = 'key' const ALIAS_RESOURCE_TYPE = 'alias' const MRK_RESOURCE_ID_PREFIX = 'mrk-' const VALID_RESOURCE_TYPES = [KEY_RESOURCE_TYPE, ALIAS_RESOURCE_TYPE] /** * Returns a parsed ARN if a valid AWS KMS Key ARN. * If the request is a valid resource the function * will return false. * However if the ARN is malformed this function throws an error, */ export function parseAwsKmsKeyArn( kmsKeyArn: string ): ParsedAwsKmsKeyArn | false { /* Precondition: A KMS Key Id must be a non-null string. */ needs( kmsKeyArn && typeof kmsKeyArn === 'string', 'KMS key arn must be a non-null string.' ) const parts = kmsKeyArn.split(':') /* Check for early return (Postcondition): A valid ARN has 6 parts. */ if (parts.length === 1) { /* Exceptional Postcondition: Only a valid AWS KMS resource. * This may result in this function being called twice. * However this is the most correct behavior. */ parseAwsKmsResource(kmsKeyArn) return false } /* See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms * arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 * arn:aws:kms:us-east-1:123456789012:alias/example-alias */ const [ arnLiteral, partition, service, region = '', account = '', resource = '', ] = parts const [resourceType, ...resourceSection] = resource.split('/') //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The resource section MUST be non-empty and MUST be split by a //# single "/" any additional "/" are included in the resource id const resourceId = resourceSection.join('/') /* If this is a valid AWS KMS Key ARN, return the parsed ARN */ needs( //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# MUST start with string "arn" arnLiteral === ARN_PREFIX && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The partition MUST be a non-empty partition && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The service MUST be the string "kms" service === KMS_SERVICE && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The region MUST be a non-empty string region && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The account MUST be a non-empty string account && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The resource type MUST be either "alias" or "key" VALID_RESOURCE_TYPES.includes(resourceType) && //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 //# The resource id MUST be a non-empty string resourceId, 'Malformed arn.' ) return { Partition: partition, Region: region, AccountId: account, ResourceType: resourceType, ResourceId: resourceId, } } export function getRegionFromIdentifier(kmsKeyIdentifier: string): string { const awsKmsKeyArn = parseAwsKmsKeyArn(kmsKeyIdentifier) return awsKmsKeyArn ? awsKmsKeyArn.Region : '' } export function parseAwsKmsResource( resource: string ): Pick<ParsedAwsKmsKeyArn, 'ResourceType' | 'ResourceId'> { /* Precondition: An AWS KMS resource can not have a `:`. * That would make it an ARNlike. */ needs(resource.split(':').length === 1, 'Malformed resource.') /* `/` is a valid values in an AWS KMS Alias name. */ const [head, ...tail] = resource.split('/') /* Precondition: A raw identifer is only an alias or a key. */ needs(head === ALIAS_RESOURCE_TYPE || !tail.length, 'Malformed resource.') const [resourceType, resourceId] = head === ALIAS_RESOURCE_TYPE ? [ALIAS_RESOURCE_TYPE, tail.join('/')] : [KEY_RESOURCE_TYPE, head] return { ResourceType: resourceType, ResourceId: resourceId, } } export function validAwsKmsIdentifier( kmsKeyIdentifier: string ): | ParsedAwsKmsKeyArn | Pick<ParsedAwsKmsKeyArn, 'ResourceType' | 'ResourceId'> { return ( parseAwsKmsKeyArn(kmsKeyIdentifier) || parseAwsKmsResource(kmsKeyIdentifier) ) } //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 //# This function MUST take a single AWS KMS ARN export function isMultiRegionAwsKmsArn( kmsIdentifier: string | ParsedAwsKmsKeyArn ): boolean { //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 //# If the input is an invalid AWS KMS ARN this function MUST error. const awsKmsKeyArn = typeof kmsIdentifier === 'string' ? parseAwsKmsKeyArn(kmsIdentifier) : kmsIdentifier /* Precondition: The kmsIdentifier must be an ARN. */ needs(awsKmsKeyArn, 'AWS KMS identifier is not an ARN') const { ResourceType, ResourceId } = awsKmsKeyArn //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 //# If resource type is "alias", this is an AWS KMS alias ARN and MUST //# return false. // //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 //# If resource type is "key" and resource ID starts with //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. // //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 //# If resource type is "key" and resource ID does not start with "mrk-", //# this is a (single-region) AWS KMS key ARN and MUST return false. return ( ResourceType === KEY_RESOURCE_TYPE && ResourceId.startsWith(MRK_RESOURCE_ID_PREFIX) ) } //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 //# This function MUST take a single AWS KMS identifier export function isMultiRegionAwsKmsIdentifier(kmsIdentifier: string): boolean { //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 //# If the input starts with "arn:", this MUST return the output of //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this //# input. if (kmsIdentifier.startsWith('arn:')) { return isMultiRegionAwsKmsArn(kmsIdentifier) } else if (kmsIdentifier.startsWith('alias/')) { //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 //# If the input starts with "alias/", this an AWS KMS alias and //# not a multi-Region key id and MUST return false. return false } else if (kmsIdentifier.startsWith(MRK_RESOURCE_ID_PREFIX)) { //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 //# If the input starts //# with "mrk-", this is a multi-Region key id and MUST return true. return true } //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 //# If //# the input does not start with any of the above, this is not a multi- //# Region key id and MUST return false. return false } /* Returns a boolean representing whether two AWS KMS Key IDs should be considered equal. * For everything except MRK-indicating ARNs, this is a direct comparison. * For MRK-indicating ARNs, this is a comparison of every ARN component except region. * Throws an error if the IDs are not explicitly equal and at least one of the IDs * is not a valid AWS KMS Key ARN or alias name. */ //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 //# The caller MUST provide: export function mrkAwareAwsKmsKeyIdCompare( keyId1: string, keyId2: string ): boolean { //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 //# If both identifiers are identical, this function MUST return "true". if (keyId1 === keyId2) return true //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 //# Otherwise if either input is not identified as a multi-Region key //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then //# this function MUST return "false". const arn1 = parseAwsKmsKeyArn(keyId1) const arn2 = parseAwsKmsKeyArn(keyId2) if (!arn1 || !arn2) return false if (!isMultiRegionAwsKmsArn(arn1) || !isMultiRegionAwsKmsArn(arn2)) return false //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 //# Otherwise if both inputs are //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- //# aws-kms-multi-region-key), this function MUST return the result of //# comparing the "partition", "service", "accountId", "resourceType", //# and "resource" parts of both ARN inputs. return ( arn1.Partition === arn2.Partition && arn1.AccountId === arn2.AccountId && arn1.ResourceType === arn2.ResourceType && arn1.ResourceId === arn2.ResourceId ) } /* Manually construct a new MRK ARN that looks like the old ARN except the region is replaced by a new region. * Throws an error if the input parsed ARN is not an MRK */ export function constructArnInOtherRegion( parsedArn: ParsedAwsKmsKeyArn, region: string ): string { /* Precondition: Only reconstruct a multi region ARN. */ needs( isMultiRegionAwsKmsArn(parsedArn), 'Cannot attempt to construct an ARN in a new region from an non-MRK ARN.' ) const { Partition, AccountId, ResourceType, ResourceId } = parsedArn return [ ARN_PREFIX, Partition, KMS_SERVICE, region, AccountId, ResourceType + '/' + ResourceId, ].join(':') }