Blowfish encrypt/decrypt is no longer supported on miva servers. You can migrate if you ask them to turn on "legacy" settings at the server level. The problem in detail is:
This is almost certainly an OpenSSL 3 / Legacy Provider issue. Here's the breakdown:
What's happening:
Miva's bf_encrypt() is a MivaScript wrapper around OpenSSL's Blowfish implementation. In OpenSSL 3.0+, Blowfish was moved out of the default provider and into the legacy provider, which is not loaded automatically. When Blowfish is unavailable, the cipher initialization silently fails — the function returns 1 (as if it succeeded) but the encrypted output variable ends up empty with len = 0. This exactly matches the behavior you're describing.
Why it works in dev but not on the other site:
Your dev environment is almost certainly running an older OS (e.g., Ubuntu 20.04, CentOS 7, RHEL 7/8) with OpenSSL 1.1.x, where Blowfish is fully supported by default. The other site is likely on a newer server (Ubuntu 22.04+, RHEL 9, Debian 12, etc.) running OpenSSL 3.x, where Blowfish is legacy-only.
How to confirm: Check the OpenSSL version on each server:
openssl version
Dev will likely show 1.1.x, the broken site will show 3.x.
Your options:
- Switch to
<strong>crypto_evp_encrypt()</strong> with AES — this is Miva's recommended modern path and works on all current servers:crypto_evp_encrypt( 'aes-256-cbc', key, iv, plaintext, l.encrypted )
Note: key must be exactly the length required by the cipher (crypto_cipher_key_length()), same for the IV. - Enable the OpenSSL legacy provider on the server — if you control the server and need a quick fix, you can enable it in
openssl.cnf. But this is a workaround, not a long-term solution, and may not be available in managed/cloud hosting environments. - Check with Miva support
— if you're on Miva's managed hosting, they may have disabled the
legacy provider as part of a security hardening update, and you'd need
to request a migration path.
The bottom line: bf_encrypt() is a legacy function that depends on OpenSSL's legacy Blowfish support, and that support is no longer active by default on modern OS/server stacks. Migrating to crypto_evp_encrypt() with AES is the right fix.
Example code logic: Random, based off a use case, but basic principle is here.
<strong>ciphername</strong> — just a string telling it which algorithm to use. Use 'aes-256-cbc'. It's the modern standard, universally supported.<strong>key</strong> — a secret password, BUT it must be exactly the right length for the cipher. For aes-256-cbc, that's exactly 32 characters.<strong>iv</strong> (initialization vector) — think of it as a "salt" that makes the same plaintext produce different encrypted output each time if you change it. For aes-256-cbc, it must be exactly 16 characters.
For simple/static use like yours, you can just hardcode it. It doesn't
need to be secret, just consistent between encrypt and decrypt.
<MvFUNCTION NAME="Encrypt_Token" PARAMETERS="plaintext" STANDARDOUTPUTLEVEL="">
LOCAL l.cipher, l.key, l.iv, l.encrypted, l.success, l.link_param
l.cipher = 'aes-256-cbc'
l.key = 'MySuperSecretKey1234567890123456' | 32 chars exactly
l.iv = 'MyInitVector1234' | 16 chars exactly
l.success = crypto_evp_encrypt( l.cipher, l.key, l.iv, l.plaintext, l.encrypted )
IF NOT l.success THEN
RETURN ''
ENDIF
| Base64 encode so it's safe in a URL
RETURN crypto_base64_encode( l.encrypted )
</MvFUNCTION>
Decrypt
<MvFUNCTION NAME="Decrypt_Token" PARAMETERS="token" STANDARDOUTPUTLEVEL="">
LOCAL l.cipher, l.key, l.iv, l.decoded, l.decrypted, l.success
l.cipher = 'aes-256-cbc'
l.key = 'MySuperSecretKey1234567890123456' | 32 chars exactly
l.iv = 'MyInitVector1234' | 16 chars exactly
| Base64 decode first before decrypting
l.decoded = crypto_base64_decode( l.token )
l.success = crypto_evp_decrypt( l.cipher, l.key, l.iv, l.decoded, l.decrypted )
IF NOT l.success THEN
RETURN ''
ENDIF
RETURN l.decrypted
</MvFUNCTION>
Parse the Decrypted Value
<MvFUNCTION NAME="Parse_Token" PARAMETERS="decrypted" STANDARDOUTPUTLEVEL="">
LOCAL l.pos, l.id, l.email
l.pos = indexof( ':', l.decrypted, 1 )
IF NOT l.pos THEN
RETURN 0 | No colon found, bad token
ENDIF
l.id = substring( l.decrypted, 1, l.pos - 1 )
l.email = substring( l.decrypted, l.pos + 1, len( l.decrypted ) )
| Do whatever you need with l.id and l.email here
RETURN 1
</MvFUNCTION>
Sanity Check (length validation)
<MvFUNCTION NAME="Check_Cipher_Lengths" STANDARDOUTPUTLEVEL="">
LOCAL l.cipher, l.key, l.iv, l.key_len, l.iv_len, l.success
l.cipher = 'aes-256-cbc'
l.key = 'MySuperSecretKey1234567890123456'
l.iv = 'MyInitVector1234'
l.success = crypto_cipher_key_length( l.cipher, l.key_len )
l.success = crypto_cipher_iv_length( l.cipher, l.iv_len )
IF len( l.key ) NE l.key_len THEN
| Key length mismatch - will silently fail
RETURN 0
ENDIF
IF len( l.iv ) NE l.iv_len THEN
| IV length mismatch - will silently fail
RETURN 0
ENDIF
RETURN 1
</MvFUNCTION>
The base64 encode/decode step is essential when putting encrypted data in a URL — raw encrypted bytes contain characters that will break URLs. Encode before putting in the link, decode before decrypting.
The key and IV must be identical on both the encrypt and decrypt side. Since you're doing this within the same Miva store, the easiest approach is to define them once in a global variable or store setting that both sides reference.
https://www.scotsscripts.com/mvblog/encryption-2026-miva-encryption-standards-sans-blowfish.html