Skip to content

⚡️ Speed up method AuthService.decrypt_api_key by 16% in PR #11639 (docs-chat-refactor-and-screenshots)#11644

Closed
codeflash-ai[bot] wants to merge 6 commits into
docs-1.8-releasefrom
codeflash/optimize-pr11639-2026-02-07T00.49.34
Closed

⚡️ Speed up method AuthService.decrypt_api_key by 16% in PR #11639 (docs-chat-refactor-and-screenshots)#11644
codeflash-ai[bot] wants to merge 6 commits into
docs-1.8-releasefrom
codeflash/optimize-pr11639-2026-02-07T00.49.34

Conversation

@codeflash-ai
Copy link
Copy Markdown
Contributor

@codeflash-ai codeflash-ai Bot commented Feb 7, 2026

⚡️ This pull request contains optimizations for PR #11639

If you approve this dependent PR, these changes will be merged into the original PR branch docs-chat-refactor-and-screenshots.

This PR will be automatically closed if the original PR is merged.


📄 16% (0.16x) speedup for AuthService.decrypt_api_key in src/backend/base/langflow/services/auth/service.py

⏱️ Runtime : 240 microseconds 208 microseconds (best of 26 runs)

📝 Explanation and details

The optimized code achieves a 15% speedup through two key optimizations:

1. Fernet Instance Caching (Primary Optimization)

The original code recreates the Fernet object on every call to _get_fernet(), which involves expensive cryptographic key processing. The line profiler shows:

  • Original: _ensure_valid_key() takes 61.3% of _get_fernet() time (6.35ms), and Fernet(valid_key) takes 20.7% (2.14ms)
  • Optimized: These operations are cached, only executing on first call or secret key change

The optimization adds _fernet_cache and _cached_secret_key instance variables in __init__(). In _get_fernet(), it checks if the cache is valid before recreating the Fernet object. With 212 calls to _get_fernet(), but only 13 cache misses (line profiler shows 13 hits for _ensure_valid_key), this means 199 calls avoided expensive key derivation, reducing _get_fernet() total time from 10.37ms to 2.57ms (~75% reduction).

2. Fast-Path for Invalid Inputs

The original code called logger.debug() for every invalid input, taking 31.8% of decrypt_api_key() time (14.69ms for 910 hits). The optimized version reorders the condition check (if not encrypted_api_key or not isinstance(...)) and removes the debug log entirely, reducing overhead from 16.1ms to 0.87ms per invalid input check.

Impact on Test Cases

  • Batch decryption tests (e.g., test_large_scale_decryption_batch with 200 tokens, test_decrypt_many_plaintext_keys with 500 keys) benefit most since the Fernet instance is reused across all decryptions
  • Invalid input tests (e.g., test_decrypt_empty_and_none_batch with 500 empty/None values) see faster returns due to the optimized fast-path
  • Single decryption tests show minimal improvement since caching benefits are proportional to call frequency

The optimizations are safe because the Fernet instance depends only on the secret key, which is checked on every call to detect configuration changes.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 57 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Click to see Generated Regression Tests

import base64
import random
import types

import pytest # used for our unit tests
from cryptography.fernet import Fernet
from langflow.services.auth.service import AuthService

Note:

The tests below import and use the real AuthService class from its module path.

In case the real package/module is not available in the test environment, these imports

will raise ImportError when running tests. The test suite assumes the codebase layout

matches the original source path: langflow.services.auth.service.AuthService

Helper to build a settings-like object expected by AuthService.init

def make_settings_with_secret(secret_value: str):
"""
Construct a minimal object graph that matches what AuthService expects:
settings.auth_settings.SECRET_KEY.get_secret_value() -> secret_value

We avoid defining domain classes and instead use simple namespaces for non-domain
configuration objects. This keeps the object lightweight and deterministic.
"""
# SECRET_KEY.get_secret_value() should return the provided secret string
secret_key_obj = types.SimpleNamespace(get_secret_value=lambda: secret_value)
auth_settings = types.SimpleNamespace(SECRET_KEY=secret_key_obj)
settings = types.SimpleNamespace(auth_settings=auth_settings)
return settings

Helper replicating the short-key branch of _ensure_valid_key to derive a Fernet key

This mirrors the implementation in AuthService._ensure_valid_key when raw_key length < 32

def derive_fernet_key_from_short_secret(raw_key: str) -> bytes:
# Seed the random module with the raw_key to get deterministic bytes (same as AuthService)
random.seed(raw_key)
key_bytes = bytes(random.getrandbits(8) for _ in range(32))
return base64.urlsafe_b64encode(key_bytes)

def test_decrypt_api_key_invalid_inputs():
# Create service instance with a deterministic short secret so it constructs a Fernet
settings = make_settings_with_secret("short_secret") # length < 32 -> deterministic key branch
service = AuthService(settings)

# None is invalid -> should return empty string
codeflash_output = service.decrypt_api_key(None)

# Empty string is invalid -> should return empty string
codeflash_output = service.decrypt_api_key("")

# Non-string types are invalid -> should return empty string
codeflash_output = service.decrypt_api_key(12345)
codeflash_output = service.decrypt_api_key(b"bytes-not-str")

def test_decrypt_api_key_plaintext_returned_as_is():
# Plaintext strings not starting with 'gAAAAA' should be returned unchanged
settings = make_settings_with_secret("short_secret")
service = AuthService(settings)

# Typical API key that is plaintext (doesn't start with Fernet prefix) should passthrough
plaintext = "plain_api_key_123"
codeflash_output = service.decrypt_api_key(plaintext)

# Even if the string contains 'gAAAA' somewhere inside, but not starting with 'gAAAAA',
# it should still be returned unchanged
tricky_plaintext = "prefix_gAAAA_inside_but_not_start"
codeflash_output = service.decrypt_api_key(tricky_plaintext)

def test_decrypt_api_key_successful_decryption_with_deterministic_short_secret():
# Use a short secret so AuthService will use the deterministic random-based key logic
raw_secret = "short_secret"
settings = make_settings_with_secret(raw_secret)
service = AuthService(settings)

# Derive the exact Fernet key the service will construct so we can encrypt a token
key_for_encryption = derive_fernet_key_from_short_secret(raw_secret)
f = Fernet(key_for_encryption)

# Encrypt a sample plaintext API key using the derived key
plaintext = b"my_encrypted_api_key"
token_bytes = f.encrypt(plaintext)  # returns bytes
token_str = token_bytes.decode()  # service expects string input starting with 'gAAAAA'

# Service should correctly decrypt and return the original plaintext as a string
codeflash_output = service.decrypt_api_key(token_str); result = codeflash_output

def test_decrypt_api_key_wrong_secret_returns_empty():
# Create two different short secrets so that keys differ deterministically
encrypt_secret = "encrypt_me" # used to create token
service_secret = "service_has_different_secret" # used by the service to decrypt

# Build a Fernet for encryption using the encrypt_secret
encryption_key = derive_fernet_key_from_short_secret(encrypt_secret)
f_encrypt = Fernet(encryption_key)
token = f_encrypt.encrypt(b"token_that_will_not_decrypt").decode()

# Instantiate the service with a different secret -> decryption should fail and return ""
settings = make_settings_with_secret(service_secret)
service = AuthService(settings)

codeflash_output = service.decrypt_api_key(token)

def test_decrypt_api_key_retry_path_primary_fails_secondary_succeeds(monkeypatch):
"""
Force the primary decryption attempt to raise an exception while the secondary
(retry) call succeeds. We patch AuthService._get_fernet to return a custom object
whose decrypt method behaves differently depending on input type.
This verifies the retry logic inside decrypt_api_key.
"""
settings = make_settings_with_secret("short_secret")
service = AuthService(settings)

# Create a fake token string that looks like a Fernet token (starts with gAAAAA)
fake_token = "gAAAAA-this-is-a-fake-token"

# Define a decrypt function that simulates:
# - raising an exception when a bytes input is provided (primary attempt)
# - returning the correct decrypted bytes when a str input is provided (secondary attempt)
call_state = {"calls": 0}

def simulated_decrypt(input_value):
    call_state["calls"] += 1
    # First call will be with bytes (because decrypt_api_key does .encode())
    if isinstance(input_value, (bytes, bytearray)):
        raise Exception("simulated primary failure")
    # Second call receives the raw string (retry path) -> return a bytes object as Fernet does
    if isinstance(input_value, str):
        return b"recovered_value"
    # Any other types -> raise to be explicit
    raise TypeError("unexpected input type to simulated_decrypt")

# Monkeypatch the _get_fernet to return an object with our simulated decrypt
fake_fernet = types.SimpleNamespace(decrypt=simulated_decrypt)
monkeypatch.setattr(AuthService, "_get_fernet", lambda self: fake_fernet)

# Now call decrypt_api_key and assert that the retry path returned the recovered_value
codeflash_output = service.decrypt_api_key(fake_token); result = codeflash_output

def test_decrypt_api_key_both_attempts_fail_logs_and_returns_empty(monkeypatch):
"""
Simulate both decrypt attempts failing to ensure decrypt_api_key returns an empty string.
We patch _get_fernet so decrypt always raises.
"""
settings = make_settings_with_secret("short_secret")
service = AuthService(settings)

fake_token = "gAAAAA-still-fake"

def always_fail(_):
    raise ValueError("always fail")

fake_fernet = types.SimpleNamespace(decrypt=always_fail)
monkeypatch.setattr(AuthService, "_get_fernet", lambda self: fake_fernet)

codeflash_output = service.decrypt_api_key(fake_token)

def test_large_scale_decryption_batch():
"""
Create a moderate batch of encrypted tokens (below 1000 elements as required)
and decrypt them all to validate performance and correctness at scale.
We keep the batch size reasonably large (200) while staying within the constraint.
"""
raw_secret = "short_secret_batch"
settings = make_settings_with_secret(raw_secret)
service = AuthService(settings)

# Derive the matching Fernet key and instantiate a local Fernet for encryption
key = derive_fernet_key_from_short_secret(raw_secret)
f = Fernet(key)

batch_size = 200  # sufficiently large but well below 1000
tokens = []
expected_plaintexts = []

# Generate a number of unique plaintexts and their corresponding encrypted tokens
for i in range(batch_size):
    plaintext = f"batch_plain_{i}".encode()
    token = f.encrypt(plaintext).decode()
    tokens.append(token)
    expected_plaintexts.append(plaintext.decode())

# Decrypt all tokens using the service and ensure results match expected plaintexts
results = [service.decrypt_api_key(t) for t in tokens]

def test_non_fernet_but_prefix_present_returns_empty_if_decryption_fails(monkeypatch):
"""
If a string starts with gAAAAA but is not a valid token, the function will attempt to
decrypt it. If decryption fails, it should return an empty string. This test verifies that.
"""
settings = make_settings_with_secret("short_secret")
service = AuthService(settings)

bogus_token = "gAAAAA-not-a-valid-token"

# Ensure default _get_fernet is used (constructed from the secret), but encrypt a different valid token
# So decrypting bogus_token should fail.
codeflash_output = service.decrypt_api_key(bogus_token)

codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

#------------------------------------------------
from unittest.mock import MagicMock, Mock, patch

import pytest
from cryptography.fernet import Fernet, InvalidToken
from langflow.services.auth.service import AuthService

def test_decrypt_plaintext_api_key():
"""Test that plaintext API keys (not starting with 'gAAAAA') are returned as-is."""
# Create a mock settings service
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

# Create AuthService instance
auth_service = AuthService(mock_settings_service)

# Test with plaintext API key
plaintext_key = "my_plaintext_api_key_12345"
codeflash_output = auth_service.decrypt_api_key(plaintext_key); result = codeflash_output

def test_decrypt_empty_string():
"""Test that empty string input returns empty string."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)
codeflash_output = auth_service.decrypt_api_key(""); result = codeflash_output

def test_decrypt_none_input():
"""Test that None input returns empty string."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)
# Pass None - the function should handle it gracefully
codeflash_output = auth_service.decrypt_api_key(None); result = codeflash_output

def test_decrypt_non_string_input():
"""Test that non-string input returns empty string."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Test with various non-string types
codeflash_output = auth_service.decrypt_api_key(123)
codeflash_output = auth_service.decrypt_api_key(45.67)
codeflash_output = auth_service.decrypt_api_key([])
codeflash_output = auth_service.decrypt_api_key({})

def test_decrypt_whitespace_only_string():
"""Test that whitespace-only strings are treated as empty."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Whitespace is not empty string, so should attempt to process
# But spaces don't start with gAAAAA, so should return as-is
codeflash_output = auth_service.decrypt_api_key("   "); result = codeflash_output

def test_decrypt_very_long_plaintext_key():
"""Test decryption of very long plaintext API key."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create a very long plaintext key
long_key = "a" * 10000
codeflash_output = auth_service.decrypt_api_key(long_key); result = codeflash_output

def test_decrypt_special_characters_plaintext():
"""Test decryption of plaintext key with special characters."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Plaintext key with special characters
special_key = "key!@#$%^&*()_+-=[]{}|;:',.<>?/~`"
codeflash_output = auth_service.decrypt_api_key(special_key); result = codeflash_output

def test_decrypt_unicode_characters_plaintext():
"""Test decryption of plaintext key with unicode characters."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Plaintext key with unicode characters
unicode_key = "key_with_émojis_\u2764\ufe0f_and_symbols_\u4e2d\u6587"
codeflash_output = auth_service.decrypt_api_key(unicode_key); result = codeflash_output

def test_decrypt_case_sensitive_gaaaaa_prefix():
"""Test that gAAAAA prefix check is case-sensitive."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Test with uppercase version of prefix - should be treated as plaintext
codeflash_output = auth_service.decrypt_api_key("GAAAAA_some_key"); result = codeflash_output

# Test with mixed case
codeflash_output = auth_service.decrypt_api_key("gAAAAa_some_key"); result = codeflash_output

def test_decrypt_with_short_secret_key():
"""Test decryption when secret key is shorter than minimum length."""
mock_settings_service = Mock()
# Use a short secret key (less than 32 chars)
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "short_key"

auth_service = AuthService(mock_settings_service)

# Plaintext key should still work
plaintext_key = "my_api_key_plaintext"
codeflash_output = auth_service.decrypt_api_key(plaintext_key); result = codeflash_output

def test_decrypt_many_plaintext_keys():
"""Test decryption of many plaintext API keys to assess performance."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create and decrypt 500 plaintext keys
num_keys = 500
plaintext_keys = [f"api_key_{i}" for i in range(num_keys)]

results = [auth_service.decrypt_api_key(key) for key in plaintext_keys]

def test_decrypt_keys_with_varying_lengths():
"""Test decryption of plaintext keys with various lengths."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create keys with lengths from 1 to 1000 characters
test_lengths = [1, 10, 50, 100, 500, 1000]

for length in test_lengths:
    key = "k" * length
    codeflash_output = auth_service.decrypt_api_key(key); result = codeflash_output

def test_decrypt_empty_and_none_batch():
"""Test batch processing with empty strings and None values."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create a batch of 250 empty strings and 250 None values
test_inputs = [""] * 250 + [None] * 250

results = [auth_service.decrypt_api_key(val) for val in test_inputs]

def test_decrypt_consecutive_calls_same_key():
"""Test that consecutive decryption calls with same key are consistent."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Decrypt the same key 100 times
test_key = "my_consistent_api_key"
results = [auth_service.decrypt_api_key(test_key) for _ in range(100)]

def test_decrypt_keys_with_common_prefixes():
"""Test decryption of plaintext keys with common prefixes."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create 200 keys with common prefix but different suffixes
prefix = "api_key_v1_"
keys = [f"{prefix}{i}" for i in range(200)]

results = [auth_service.decrypt_api_key(key) for key in keys]

def test_decrypt_extreme_length_plaintext():
"""Test decryption with extremely long plaintext key (but reasonable for API keys)."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create a very long but realistic API key
extreme_key = "sk-" + "a" * 2000
codeflash_output = auth_service.decrypt_api_key(extreme_key); result = codeflash_output

def test_decrypt_performance_many_non_string_calls():
"""Test performance when handling many non-string inputs."""
mock_settings_service = Mock()
mock_settings_service.auth_settings.SECRET_KEY.get_secret_value.return_value = "test_secret_key_minimum_32_chars_"

auth_service = AuthService(mock_settings_service)

# Create a variety of non-string inputs
non_string_inputs = []
non_string_inputs.extend([None] * 100)
non_string_inputs.extend([123] * 100)
non_string_inputs.extend([45.67] * 100)
non_string_inputs.extend([[] for _ in range(100)])

results = [auth_service.decrypt_api_key(val) for val in non_string_inputs]

To edit these changes git checkout codeflash/optimize-pr11639-2026-02-07T00.49.34 and push.

Codeflash

mendonk and others added 6 commits February 6, 2026 17:02
The optimized code achieves a **15% speedup** through two key optimizations:

## 1. Fernet Instance Caching (Primary Optimization)
The original code recreates the `Fernet` object on every call to `_get_fernet()`, which involves expensive cryptographic key processing. The line profiler shows:
- **Original**: `_ensure_valid_key()` takes 61.3% of `_get_fernet()` time (6.35ms), and `Fernet(valid_key)` takes 20.7% (2.14ms)
- **Optimized**: These operations are cached, only executing on first call or secret key change

The optimization adds `_fernet_cache` and `_cached_secret_key` instance variables in `__init__()`. In `_get_fernet()`, it checks if the cache is valid before recreating the Fernet object. With 212 calls to `_get_fernet()`, but only 13 cache misses (line profiler shows 13 hits for `_ensure_valid_key`), this means **199 calls avoided expensive key derivation**, reducing `_get_fernet()` total time from 10.37ms to 2.57ms (**~75% reduction**).

## 2. Fast-Path for Invalid Inputs
The original code called `logger.debug()` for every invalid input, taking 31.8% of `decrypt_api_key()` time (14.69ms for 910 hits). The optimized version reorders the condition check (`if not encrypted_api_key or not isinstance(...)`) and removes the debug log entirely, reducing overhead from 16.1ms to 0.87ms per invalid input check.

## Impact on Test Cases
- **Batch decryption tests** (e.g., `test_large_scale_decryption_batch` with 200 tokens, `test_decrypt_many_plaintext_keys` with 500 keys) benefit most since the Fernet instance is reused across all decryptions
- **Invalid input tests** (e.g., `test_decrypt_empty_and_none_batch` with 500 empty/None values) see faster returns due to the optimized fast-path
- **Single decryption tests** show minimal improvement since caching benefits are proportional to call frequency

The optimizations are safe because the Fernet instance depends only on the secret key, which is checked on every call to detect configuration changes.
@codeflash-ai codeflash-ai Bot added the ⚡️ codeflash Optimization PR opened by Codeflash AI label Feb 7, 2026
@github-actions github-actions Bot added the community Pull Request from an external contributor label Feb 7, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (docs-1.8-release@cdacc17). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                 @@
##             docs-1.8-release   #11644   +/-   ##
===================================================
  Coverage                    ?   35.21%           
===================================================
  Files                       ?     1521           
  Lines                       ?    72927           
  Branches                    ?    10936           
===================================================
  Hits                        ?    25681           
  Misses                      ?    45851           
  Partials                    ?     1395           
Flag Coverage Δ
backend 55.67% <100.00%> (?)
lfx 42.11% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/services/auth/service.py 66.82% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from docs-chat-refactor-and-screenshots to docs-1.8-release February 10, 2026 16:03
@codeflash-ai codeflash-ai Bot closed this Feb 10, 2026
@codeflash-ai
Copy link
Copy Markdown
Contributor Author

codeflash-ai Bot commented Feb 10, 2026

This PR has been automatically closed because the original PR #11639 by mendonk was closed.

@codeflash-ai codeflash-ai Bot deleted the codeflash/optimize-pr11639-2026-02-07T00.49.34 branch February 10, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI community Pull Request from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant