Protéger des valeurs sensibles

Effectué

Avec un nombre croissant d’intégrations à des systèmes externes, il n’est pas rare, dans du code AL, d’utiliser des secrets tels que des informations d’identification et d’autres valeurs textuelles sensibles. Compte tenu de la nécessité d’éviter leur révélation lors d’un débogage, ce dernier était souvent bloqué par des stratégies d’exposition des ressources, au détriment d’un dépannage facile.

Pour prendre en charge l’activation du débogage, tout en évitant la révélation d’informations d’identification et d’autres valeurs textuelles sensibles, nous introduisons un nouveau type SecretText pour les variables. En outre, certains des scénarios courants dans l’application système sont pris en charge pour transmettre les paramètres SecretText pour les informations d’identification, par exemple les types HttpClient et Stockage isolé.

Le type de données SecretText est conçu pour éviter d’exposer les valeurs sensibles au moyen du débogueur AL lors d’un débogage standard ou d’instantané. Son utilisation est recommandée pour les applications qui doivent gérer tout type d’informations d’identification telles que des clés API, des jetons de licence personnalisés ou des éléments similaires.

Extraction

Lorsque des informations d’identification ne sont pas protégées par l’attribut NonDebuggable dans l’étendue d’une procédure ou dans la variable dans laquelle elles se trouvent, elles sont susceptibles d’être exposées dans une session de débogage ou un instantané pour toute leur durée de vie en code AL. Cette durée de vie peut être fractionnée en trois phases distinctes.

Des informations d’identification peuvent être extraites de plusieurs manières :

  1. Une clé API est extraite grâce à un appel au moyen d’AL HttpClient, puis utilisée comme authentification pour d’autres appels.

  2. Un jeton est extrait au moyen d’un complément de contrôle implémentant une intégration avec un fournisseur d’authentification comme OAUTH2.

  3. Un scénario personnalisé défini par le développeur crée un jeton d’authentification.

  4. Un développeur code en dur par erreur des informations d’identification dans le code à des fins de test et oublie de les supprimer.

Toute valeur de type Text ou Code peut être affectée à une valeur SecretText. Si les jetons sont extraits, puis convertis en valeur SecretText dans le cadre d’une procédure ne pouvant pas être déboguée, ils sont protégés du débogueur pendant leur durée de vie. De plus, le compilateur AL garantit que des informations d’identification codées en dur ne peuvent pas être affectées directement à une destination de type SecretText.

[NonDebuggable]

procedure RetrieveSessionToken(Credential: SecretText; TargetUri: Text) SessionToken : SecretText
  var
    Request: HttpRequestMessage;
    Response: HttpResponseMessage;
    Client: HttpClient;
    Headers: HttpHeaders;
    Content: HttpContent;
  begin
    Request.SetRequestUri(TargetUri);
    Request.GetHeaders(Headers);

    // Compose an authorization header with a secret value
    Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', Credential));
    Client.Send(Request, Response);

    ParseSessionToken(Response, SessionToken);
    exit;
  end;

[NonDebuggable]

procedure ParseSessionToken(Response: HttpResponseMessage; var SessionToken: SecretText) : Text
  begin
    // Parse the response
  end;

Transit

Des informations d’identification transitent par du code AL pour atteindre les points où elles sont utilisées. Le transit comprend les éléments suivants :

  • Affecter à des variables

  • Utiliser comme paramètre pour appeler une procédure/un déclencheur

  • Devenir la valeur de renvoi d’un appel de fonction

Le type de données SecretText garantit que la valeur ne peut toujours pas être déboguée en empêchant toute affectation d’elle-même à un type pouvant être débogué. Cette contrainte inclut le type de données Variante. Par conséquent, l’attribut NonDebuggable est requis uniquement lors de l’extraction et peut être omis pour le reste de la durée de vie d’informations d’identification, car toutes les destinations intermédiaires sont automatiquement protégées.

procedure Assignments()
  var
    PlainText: Text;
    Credential: SecretText;
  begin
    Credential := PlainText; // Allowed
    PlainText := Credential; // Blocked

    ConsumePlainText(PlainText); // Allowed
    ConsumePlainText(Credential); // Blocked

    Credential := ProduceCredential(); //Allowed
    PlainText := ProduceCredential(); // Blocked
  end;

procedure ConsumePlainText(PlainText: Text)
  begin
  end;

procedure ProduceCredential(): SecretText
  begin
  end;

Utilisation

Les informations d’identification sont utilisées lorsqu’elles permettent d’effectuer une opération. Un exemple courant est la communication avec un service web externe au moyen d’AL HttpClient où les étapes suivantes peuvent être requises :

  1. Création d’un en-tête d’authentification pour la requête avec les informations d’identification

  2. Ajout des informations d’identification au corps d’une requête pour la connexion initiale

  3. Ajout d’une clé API aux paramètres d’une requête

L’AL HttpClient et tous les types intermédiaires requis pour créer une méthode de prise en charge de requête, qui accepte le type de données SecretText, afin que les valeurs puissent être transmises directement au runtime AL sans être révélées au débogueur.

L’extrait de code suivant montre comment tous les scénarios susmentionnés peuvent être implémentés au moyen de ces méthodes :

procedure SendAuthenticatedRequestToApi(UriTemplate: Text; BearerToken: SecretText; KeyParameter: SecretText; SecretBody: SecretText)

  var
    Client: HttpClient;
    Headers: HttpHeaders;
    SecretHeader: SecretText;
    SecretUri: SecretText;
    RequestMessage: HttpRequestMessage;

  begin
    SecretHeader := SecretStrSubstNo('Bearer %1', BearerToken);
    RequestMessage.GetHeaders(Headers);

    // The header is added and remains hidden when debugging the headers
    // of the request message
    Headers.Add('Authorization', SecretHeader);

    // Headers.Contains('Authorization') - false
    // Headers.ContainsSecret('Authorization') - true
    // Headers.GetSecretValues must be used to get the values.
    // It is not possible to retrieve the header value as a plain text.

    SecretUri := SecretStrSubstNo(UriTemplate, KeyParameter);
    RequestMessage.SetSecretRequestUri(SecretUri);

    // RequestMessage.GetSecretRequestUri can be used to retrieve the request uri.
    // It cannot be retrieved by GetRequestUri as a plain text.

    RequestMessage.Content.WriteFrom(SecretBody);

    // RequestMessage.Content.ReadAs can only read back the body in a SecretText destination

    SendMessageAndHandleResponse(Client, RequestMessage);
end;

[NonDebuggable]
procedure SendMessageAndHandleResponse(Client: HttpClient; Request: HttpRequestMessage) CredentialFromResponse: SecretText

  var
    Response: HttpResponseMessage;

  begin
    Client.Send(Request, Response);
    Response.Content.ReadAs(CredentialFromResponse);
end;

La méthode Unwrap

La méthode Unwrap permet d’extraire la valeur d’un paramètre de type SecretText vers une destination textuelle pour des raisons de compatibilité. Ceci n’est autorisé que lors de la création d’applications pour l’étendue OnPrem et son utilisation génère un avertissement à moins qu’elle ne soit appelée dans une procédure avec l’attribut NonDebuggable. Il est déconseillé d’utiliser cette méthode pour toute autre utilisation que l’interopérabilité .NET.

La méthode SecretStrSubstNo

La méthode SecretStrSubstNo permet de composer différentes valeurs de type SecretText sans révéler leurs valeurs. Son comportement est identique à la méthode StrSubstNo sur les valeurs Texte avec la différence importante que ses paramètres et sa valeur de renvoi sont de type SecretText.

Quelques exemples sont illustrés dans l’extrait suivant :

procedure SecretStrSubstNoExamples()

var
  First: SecretText;
  Second: SecretText;
  Result: SecretText;

begin
  // Concatenation
  Result := SecretStrSubstNo('%1%2', First, Second);

  // Build a header value
  Result := SecretStrSubstNo('Bearer %1', First);

  // Build a comma separated value list
  Result := SecretStrSubstNo('%1,%2', First, Second);

end;