Godot: Como Fazer um Save Criptografado para Proteger o Jogo de Trapaça

Aprenda a fazer um save criptografado na Godot 4 com FileAccess.open_encrypted_with_pass para dificultar trapaca, editar o save e proteger a integridade dos dados.
Se voce ja terminou um sistema basico de persistencia e agora quer que o jogador nao abra o arquivo no bloco de notas para trocar o ouro de 100 para 999999, este post e para voce. Vamos falar de save criptografado na Godot 4: como usar FileAccess.open_encrypted_with_pass para escrever e ler dados protegidos por senha, o que essa protecao realmente entrega e, principalmente, o que ela nao entrega. A ideia de criptografar save na Godot e simples de implementar, mas cheia de armadilhas de expectativa que vale a pena entender antes de sair aplicando em tudo.
Este artigo assume que voce ja sabe salvar e carregar dados. Se ainda nao domina o basico de gravar um dicionario em disco, comece pelo nosso guia de sistema de save e load na Godot e volte aqui depois. Aqui o foco e exclusivamente seguranca e integridade.
Por que criptografar o save
Todo save em disco e, por padrao, um arquivo aberto. Se voce grava JSON ou um ConfigFile em texto puro, qualquer jogador com um editor de texto ve suas chaves (ouro, vida, fase) e muda os valores em segundos. Para muitos jogos isso e irrelevante, mas em alguns casos vira um problema:
- Trapaca em rankings e progressao competitiva. Se o jogo tem placar online, conquistas ou economia que impacta outros jogadores, um save editado polui a experiencia de todo mundo.
- Integridade dos dados. Criptografar (e validar) ajuda a detectar arquivos corrompidos ou modificados pela metade, evitando que o jogo carregue um estado invalido e trave.
- Reduzir a trapaca casual. A maior parte da trapaca em single player nao vem de hackers, e sim do jogador curioso que abre o arquivo. Um save criptografado ja elimina essa camada inteira sem esforco.
O ponto-chave: criptografar nao torna o jogo inviolavel. Torna a trapaca inconveniente o suficiente para o jogador medio desistir. Guarde essa frase, porque ela guia todas as decisoes daqui pra frente.
A forma nativa: FileAccess.open_encrypted_with_pass
A Godot 4 tem criptografia AES embutida no proprio FileAccess. Em vez de FileAccess.open, voce usa FileAccess.open_encrypted_with_pass, passando um terceiro argumento: a senha. A engine cuida de cifrar na escrita e decifrar na leitura de forma transparente.
A assinatura e esta:
var f: FileAccess = FileAccess.open_encrypted_with_pass(caminho, modo, senha)
Onde modo e FileAccess.WRITE para gravar ou FileAccess.READ para ler. O retorno e um objeto FileAccess ou null em caso de falha (arquivo inexistente, senha errada no modo de leitura, permissao negada). Sempre trate esse null, porque a Godot nao lanca excecao aqui: ela simplesmente devolve null e voce descobre no crash seguinte se nao verificou.
Depois de aberto, o arquivo funciona como qualquer outro FileAccess: voce pode usar store_var/get_var para gravar Variants binarios, ou store_string/get_as_text para texto. Para um save de dicionario, a dupla store_var/get_var e a mais pratica, mas vou mostrar tambem a versao com JSON, que da mais controle sobre o formato.
Exemplo completo: salvar e carregar um dicionario
Vamos montar um pequeno gerenciador de save. O ideal e coloca-lo num autoload para acessar de qualquer cena. Se voce ainda nao usa autoloads, o guia de autoload e singleton na Godot explica o padrao que vamos aplicar aqui.
extends Node
const CAMINHO_SAVE: String = "user://save.dat"
const SENHA: String = "minha-senha-super-secreta"
const VERSAO_SAVE: int = 1
func salvar(dados: Dictionary) -> bool:
var payload: Dictionary = {
"versao": VERSAO_SAVE,
"dados": dados,
}
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.WRITE, SENHA
)
if f == null:
push_error("Falha ao abrir o save para escrita: %s" % FileAccess.get_open_error())
return false
f.store_var(payload)
f.close()
return true
Repare em tres detalhes: o retorno bool avisa quem chamou se a operacao deu certo; o push_error com FileAccess.get_open_error() registra o codigo real do erro no log; e o payload embrulha os dados junto com um numero de versao, algo que vamos discutir mais adiante.
A leitura e o espelho disso:
func carregar() -> Dictionary:
if not FileAccess.file_exists(CAMINHO_SAVE):
return {}
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.READ, SENHA
)
if f == null:
push_error("Falha ao abrir o save para leitura: %s" % FileAccess.get_open_error())
return {}
var payload: Variant = f.get_var()
f.close()
if typeof(payload) != TYPE_DICTIONARY:
push_error("Save em formato invalido.")
return {}
var dict: Dictionary = payload
return dict.get("dados", {})
Aqui a Godot faz o trabalho pesado: se a senha estiver errada, o open_encrypted_with_pass no modo READ retorna null e caimos no tratamento de erro. Verificamos tambem se o que voltou e mesmo um Dictionary antes de confiar no conteudo, porque um arquivo truncado pode devolver lixo.
Usar dessa forma ja resolve o caso comum. Chamando salvar({"ouro": 100, "fase": 3}) e depois carregar(), voce recupera o dicionario intacto, e o arquivo em disco esta ilegivel num editor de texto.
Versao com JSON e texto
Se voce prefere JSON (por exemplo, para depurar mais facil durante o desenvolvimento ou para manter compatibilidade com outra ferramenta), da pra combinar JSON.stringify com store_string:
func salvar_json(dados: Dictionary) -> bool:
var payload: Dictionary = {"versao": VERSAO_SAVE, "dados": dados}
var texto: String = JSON.stringify(payload)
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.WRITE, SENHA
)
if f == null:
push_error("Falha ao abrir o save: %s" % FileAccess.get_open_error())
return false
f.store_string(texto)
f.close()
return true
func carregar_json() -> Dictionary:
if not FileAccess.file_exists(CAMINHO_SAVE):
return {}
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.READ, SENHA
)
if f == null:
return {}
var texto: String = f.get_as_text()
f.close()
var resultado: Variant = JSON.parse_string(texto)
if typeof(resultado) != TYPE_DICTIONARY:
push_error("JSON de save invalido.")
return {}
var dict: Dictionary = resultado
return dict.get("dados", {})
Lembre que JSON.parse_string retorna null se o texto for invalido, entao o typeof continua sendo obrigatorio. A escolha entre store_var e JSON e de gosto e de necessidade: store_var e mais compacto e preserva tipos nativos da Godot (como Vector2), enquanto JSON e portavel e legivel, mas converte tudo para tipos genericos.
A verdade sobre seguranca client-side
Agora a parte que separa quem entende o assunto de quem so copiou o codigo. A senha esta dentro do seu jogo. Se voce escreveu const SENHA: String = "...", essa string vai compilada dentro do executavel que voce distribui. Qualquer pessoa com o binario pode, com um pouco de engenharia reversa, encontrar a senha e descriptografar o save.
Isso nao e uma falha da Godot. E uma limitacao fundamental de qualquer criptografia client-side: para o programa conseguir ler o proprio arquivo, ele precisa ter a chave, e se ele tem a chave, quem controla a maquina tambem tem. Ofuscar a senha, quebra-la em pedacos ou gera-la a partir de dados do sistema apenas aumenta o trabalho do atacante, sem nunca torna-lo impossivel.
Entao, o que muda em relacao a senha fixa no binario? Na pratica, quase nada em termos de seguranca teorica. Uma senha fixa e simples e ja barra 99% dos jogadores. Truques de ofuscacao elevam a barra para quem tem ferramentas melhores, mas continua sendo uma barreira, nao um cofre. Trate a criptografia como uma cerca, nao como uma parede de concreto.
A conclusao pratica e direta: para jogos single player, criptografar e suficiente para o objetivo real (evitar edicao casual). Para qualquer coisa competitiva ou com dinheiro envolvido, a unica fonte de verdade confiavel e o servidor.
Complementos: hash para detectar adulteracao
Criptografia esconde o conteudo, mas nao te avisa se alguem mexeu no arquivo. Para detectar adulteracao, adicione um checksum. A ideia: calcule um hash dos dados, guarde esse hash junto no payload e, ao carregar, recalcule e compare. Se nao bater, o save foi corrompido ou modificado.
func _hash_dados(dados: Dictionary) -> String:
var texto: String = JSON.stringify(dados)
var ctx: HashingContext = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(texto.to_utf8_buffer())
var resultado: PackedByteArray = ctx.finish()
return resultado.hex_encode()
func salvar_com_hash(dados: Dictionary) -> bool:
var payload: Dictionary = {
"versao": VERSAO_SAVE,
"dados": dados,
"hash": _hash_dados(dados),
}
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.WRITE, SENHA
)
if f == null:
return false
f.store_var(payload)
f.close()
return true
func carregar_com_hash() -> Dictionary:
if not FileAccess.file_exists(CAMINHO_SAVE):
return {}
var f: FileAccess = FileAccess.open_encrypted_with_pass(
CAMINHO_SAVE, FileAccess.READ, SENHA
)
if f == null:
return {}
var payload: Variant = f.get_var()
f.close()
if typeof(payload) != TYPE_DICTIONARY:
return {}
var dict: Dictionary = payload
var dados: Dictionary = dict.get("dados", {})
var hash_salvo: String = dict.get("hash", "")
if _hash_dados(dados) != hash_salvo:
push_warning("Save adulterado ou corrompido: hash nao confere.")
return {}
return dados
Vale a mesma ressalva: um atacante que descobriu a senha pode recalcular o hash e regravar. O checksum brilha mesmo e contra corrupcao acidental (queda de energia durante a gravacao, disco cheio) e contra edicao ingenua. Para jogos online, a validacao definitiva mora no servidor: o cliente propoe um estado, o servidor confere as regras e so aceita o que for plausivel.
Boas praticas que fazem diferenca
Alguns habitos evitam dor de cabeca com saves protegidos:
- Sempre trate
null. Ja falamos, mas repito porque e o erro numero um. Arquivo inexistente, senha errada, disco cheio: todos retornamnullsem aviso. - Versione o formato do save. O campo
versaono payload permite migrar saves antigos quando voce mudar a estrutura dos dados numa atualizacao. Sem isso, um patch pode quebrar o save de todos os jogadores. - Use
user://para o caminho. E o unico diretorio garantidamente gravavel em todas as plataformas. Nunca grave save emres://, que e somente leitura no jogo exportado. - Nao criptografe dados que voce quer que a comunidade edite. Se o jogo e mod-friendly, criptografar o save atrapalha mais do que ajuda.
- Considere um
Resourcecustom para saves complexos. Para estados com muitos objetos e tipos aninhados, um recurso customizado organiza melhor os dados. Veja como em criar Resource custom na Godot; voce ainda pode salva-lo criptografado usandoResourceSaverou serializando viastore_var.
Quando NAO vale a pena criptografar
Fecha o raciocinio com o cenario oposto. Nem todo jogo precisa disso. Pule a criptografia quando:
- O jogo e single player casual e a trapaca so afeta o proprio jogador. Se alguem quer se dar ouro infinito no proprio save, isso e problema dele, nao seu.
- Voce quer uma comunidade de mods ativa, que depende de ler e editar arquivos.
- Voce ainda esta prototipando. Durante o desenvolvimento, saves em texto puro sao muito mais faceis de depurar. Ligue a criptografia perto do lancamento.
Criptografar tem custo: dificulta suporte (voce nao le o save do jogador que reportou um bug), depuracao e ate a propria manutencao. Aplique quando o beneficio (integridade competitiva, anti-trapaca) supera esse custo, e nao por reflexo.
Resumo
Um save criptografado na Godot 4 sai quase de graca com FileAccess.open_encrypted_with_pass: uma linha troca o FileAccess.open comum e a engine cuida do AES. Isso barra a trapaca casual, que e a mais comum. Mas lembre-se do limite real: a senha vive no binario, entao isso e uma barreira contra o jogador curioso, nao um cofre contra o atacante determinado. Combine com hash para detectar adulteracao, versione o formato para sobreviver a atualizacoes e, para qualquer coisa competitiva de verdade, deixe a palavra final com o servidor. No CursoGame.Dev a gente sempre defende a mesma coisa: entenda o que a ferramenta faz e o que ela nao faz antes de confiar nela.
Perguntas frequentes
O save criptografado da Godot e realmente seguro?
Ele e uma barreira, nao um cofre. Como a senha vive dentro do executavel, quem tem o binario pode extrai-la com engenharia reversa. Serve para impedir a edicao casual do save, mas nao substitui validacao no servidor em jogos online.
Qual funcao a Godot 4 usa para criptografar o save?
FileAccess.open_encrypted_with_pass(caminho, modo, senha). Ela abre o arquivo para escrita ou leitura ja aplicando AES com a senha informada, e retorna null se a senha estiver errada ou o arquivo nao existir.
Qual a diferenca entre criptografar o save e usar checksum?
Criptografia esconde o conteudo para dificultar a leitura e edicao. Um checksum ou hash nao esconde nada, mas detecta se o arquivo foi adulterado. Os dois se complementam: criptografe para dificultar e valide o hash para detectar corrupcao.
Quando NAO vale a pena criptografar o save?
Em jogos single player casuais, mod-friendly ou onde a trapaca so afeta o proprio jogador. Nesses casos a criptografia atrapalha depuracao, suporte e a comunidade de mods sem trazer beneficio real.


