Как хранить ключи API во Flutter: —dart-define vs .env

Если ваше приложение на Flutter использует API стороннего сервиса, для которого требуется API-ключ, то где его нужно хранить?

Согласно различным источникам, для приложений в продакшене, которым необходима максимальная безопасность:

  • API-ключ должен храниться на вашем защищенном сервере (и никогда на клиенте).
  • Он никогда не должен передаваться обратно на клиент (чтобы предотвратить атаки типа «человек посередине»).
  • Клиент должен общаться только с вашим сервером, который выступает в качестве прокси для стороннего API, который вы планируете использовать.

Это связано с тем, что хранение API-ключей на клиенте небезопасно и может вызвать различные проблемы, если они будут скомпрометированы.

Однако не все ключи одинаковы: некоторые ключи могут быть доступны клиенту, тогда как другие должны храниться в секрете на сервере (документация Stripe хорошо объясняет это).

На самом деле, один из ответов на StackOverflow хорошо подытоживает:

Принимая то или иное решение вы должны руководствоваться следующими критериями: насколько важны ключи, сколько времени или программного обеспечения вы можете себе позволить, насколько опытны хакеры, интересующиеся ключами, сколько времени они захотят потратить, какова ценность задержки до взлома ключей, в каком масштабе успешные хакеры будут распространять ключи и т.д. Маленькие фрагменты информации, такие как ключи, сложнее защитить, чем целые приложения. По своей сути, на клиентской стороне нет ничего, что нельзя было бы взломать, но можно значительно повысить уровень защиты.

Без сомнения, безопасность мобильного приложения — это обширная тема (на эту тему можно написать целые книги).

Поэтому давайте я дам контекст того, что мы здесь будем рассматривать. 👇

Контекст

Если вы, как и я, разрабатываете много открытых демо-приложений, которые могут никогда не дойти до продакшена 😅, вам может показаться удобным хранить менее чувствительные API-ключи на клиенте (по крайней мере, на ранних этапах разработки).

И когда дело доходит до API-ключей и безопасности, вам следует избегать двух основных ошибок:

  1. Загрузка секретного ключа в систему контроля версий, делая его видимым для всех в Интернете 🤯.
  2. Отказ от шифрования API-ключей, что упрощает задачу злоумышленникам по обратной разработке вашего приложения и извлечению ключей 🛠.

В этом руководстве мы узнаем, как избежать этих ошибок.

Что рассматривается в этом руководстве

Мы рассмотрим три различных метода хранения API-ключей на клиенте (вашем приложении на Flutter), а также их компромиссы:

  1. Жесткое кодирование ключей в файле .dart.
  2. Передача ключей в качестве аргументов командной строки с использованием --dart-define или --dart-define-from-file.
  3. Загрузка ключей из файла .env с помощью пакета ENVied.

По ходу дела мы будем помнить о следующих правилах:

  • Никогда не добавляйте API-ключи в систему контроля версий.
  • Если вы храните API-ключи на клиенте, обязательно зашифруйте их.

К концу руководства вы лучше поймете, как безопасно хранить API-ключи. Также я включу чеклист безопасности, который вы можете использовать в своих проектах на Flutter.

⚠️ Эти методы не являются непогрешимыми. Если у вас есть API-ключ, который вы не можете позволить себе потерять, храните его на сервере (а если вы используете Firebase, посмотрите мой гайд о защите API-ключей с помощью функций второго поколения в облаке). Безопасная клиент-серверная коммуникация включает в себя множество аспектов, выходящих за рамки данной статьи (см. ссылки внизу для получения дополнительной информации).

Готовы? Давайте начнем! 👇

1. Жесткое кодирование ключа в файле Dart

Простой и эффективный способ сохранить наш API-ключ — сохранить его в Dart-файле, например, так:

// api_key.dart
final tmdbApiKey = 'a1b2c33d4e5f6g7h8i9jakblc';

Чтобы убедиться, что ключ не добавлен в git, мы можем добавить файл .gitignore в ту же папку с таким содержимым:

# Скрыть ключ от контроля версий
api_key.dart

Если мы сделали все правильно, файл должен выглядеть так в проводнике:

Пример кода, использующего пакет dio для получения данных из API TMDB:

import 'api_key.dart'; // импортируем здесь
import 'package:dio/dio.dart';

Future<TMDBMoviesResponse> fetchMovies() async {
  final url = Uri(
    scheme: 'https',
    host: 'api.themoviedb.org',
    path: '3/movie/now_playing',
    queryParameters: {
      'api_key': tmdbApiKey, // читаем здесь
      'include_adult': 'false',
      'page': '$page',
    },
  ).toString();
  final response = await Dio().get(url);
  return TMDBMoviesResponse.fromJson(response.data);
}

Этот подход очень прост, но у него есть несколько недостатков:

  • Трудно управлять различными API-ключами для разных сред/конфигураций.
  • Ключ хранится в виде открытого текста в файле api_key.dart, что облегчает работу злоумышленнику.

Никогда не следует жестко кодировать API-ключи в исходном коде. Если вы добавите их в контроль версий по ошибке, они останутся в истории git, даже если вы их позже удалите.

Давайте рассмотрим второй вариант. 👇

2. Передача ключа с помощью —dart-define

Альтернативный подход — передать API-ключ с помощью флага --dart-define во время компиляции.

Это означает, что мы можем запустить приложение вот так:

flutter run --dart-define TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc

Затем в коде Dart можно сделать:

const tmdbApiKey = String.fromEnvironment('TMDB_KEY');
if (tmdbApiKey.isEmpty) {
  throw AssertionError('TMDB_KEY is not set');
}
// TODO: использовать API-ключ

Основное преимущество использования --dart-define заключается в том, что мы больше не храним чувствительные ключи в исходном коде.

Однако при компиляции нашего приложения ключи все равно будут включены в бинарный файл релиза. Чтобы уменьшить риски, мы можем зашифровать наш Dart-код при создании сборки (подробнее об этом ниже).

Также становится неудобно запускать приложение, если у нас много ключей:

flutter run \
  --dart-define TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc \
  --dart-define STRIPE_PUBLISHABLE_KEY=pk_test_aposdjpa309u2n230ibt23908g \
  --dart-define SENTRY_KEY=https://aoifhboisd934y2fhfe@a093qhq4.ingest.sentry.io/2130923

Есть ли лучший способ?

Новинка в Flutter 3.7: использование —dart-define-from-file

С Flutter 3.7 мы можем хранить все API-ключи в JSON-файле и передавать его в новый флаг --dart-define-from-file из командной строки.

Таким образом, мы можем сделать:

flutter run --dart-define-from-file=api-keys.json

Затем мы можем добавить все ключи в файл api-keys.json (который должен быть .gitignored):

{
  "TMDB_KEY": "a1b2c33d4e5f6g7h8i9jakblc",
  "STRIPE_PUBLISHABLE_KEY": "pk_test_aposdjpa309u2n230ibt23908g",
  "SENTRY_KEY": "https://aoifhboisd934y2fhfe@a093qhq4.ingest.sentry.io/2130923"
}

Этот метод достаточно удобен, и, если потребуется, мы можем даже комбинировать его с конфигурациями запуска.

Если вы используете IntelliJ или Android Studio, вы можете использовать конфигурации запуска/отладки для достижения того же результата.

3. Загрузка ключа из .env файла

.env — популярный формат файлов, который был создан для того, чтобы дать разработчикам одно безопасное место для хранения конфиденциальных данных приложений, таких как API-ключи.

Чтобы использовать это с Flutter, мы можем добавить .env файл в корень проекта.

Пример .env файла:

TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc
# добавьте другие ключи при необходимости

Заключение

При создании приложений на Flutter, особенно при разработке и в тестовых средах, где уровень риска ниже, вы можете использовать указанные методы для хранения и защиты API-ключей. Важно помнить, что любая информация, сохраненная на клиентской стороне, всегда подвержена рискам, поэтому используйте эти методы разумно и всегда оценивайте уровень безопасности вашего проекта.

Источник: https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/