Manipulating stored geocoordinates in cellphone photos
Programming Snapshot – Go Geofuzzer
Mike Schilli loves his privacy. That's why he's created a Go program that adds a geo-obfuscation layer to cellphone photos before they are published on online platforms to prevent inquisitive minds from inferring the location.
If you sell your stuff online, you might overlook the potential risk of sales-promoting cellphone photos revealing highly sensitive private information. When you take a picture of the goods at home with your cellphone, the image file may also contain the geodata with which the private address can be determined to within a few yards. Large sales platforms generally do not publish this meta-information, but who wants to give away more information than is absolutely necessary on Ebay or Facebook?
The cellphone also erases geodata directly if desired – but then it looks as if the user has something to hide. That's why the self-written Go program in this issue adds a geo-obfuscation layer to image files to make sure that the geocoordinates are randomly blurred. From this, it might be possible to determine the seller's location down to the neighborhood, but not the exact address.
Matches in the Radius
The procedure's goal is to move a photo's geotags randomly to an area within a defined action radius. If several snapshots are made, the target values are all within the action radius. In order to avoid anybody determining the center – and thus the location of the photographer – by analyzing hundreds of shots, the geofuzzer also shifts the center of the random circle to a neighboring area beforehand. To do this, it uses fixed, but secret, values for latitude and longitude (Figure 1).
An image file's geolocation is contained in the JPEG format's Exif tags [1] and can be read out with tools, such as exiftool
, or on the GeoImgr website [2]. The latter even conveniently displays the shot's location on a Google map.
Figures 2 and 3 each show the photo's geotags – a picture of a box with a Google Voice Kit I wanted to sell on Ebay. The original in Figure 2 shows my home address in San Francisco where I snapped the photo in my study. After calling the geofuzz
program, the image file's geolocation shifts further north to the Financial District. Figure 3 shows additional target values after several successive calls to the fuzzer, which scattered the results within the set action radius.
The geofuzz
program generated from Listing 1 expects the name of the JPEG file to be manipulated on the command line, as shown in Listing 2. For the user to follow along, the running program prints both the original and the modified geolocations on Stdout
. The fuzzer modifies the specified file directly, and the user can now post it without revealing too much about their location.
Listing 1
geofuzz.go
001 package main 002 003 import ( 004 "bytes" 005 "fmt" 006 exif "github.com/xor-gate/goexif2/exif" 007 "math" 008 "math/rand" 009 "os" 010 "os/exec" 011 "path/filepath" 012 "time" 013 ) 014 015 func usage(msg string) { 016 fmt.Printf("%s\n", msg) 017 fmt.Printf("usage: %s image.jpg\n", 018 filepath.Base(os.Args[0])) 019 os.Exit(1) 020 } 021 022 func main() { 023 if len(os.Args) != 2 { 024 usage("Missing argument") 025 } 026 027 img := os.Args[1] 028 029 lat, lon, err := geopos(img) 030 if err != nil { 031 panic(err) 032 } 033 034 latFuzz, lonFuzz := fuzz(lat, lon) 035 036 fmt.Printf("Was: %f,%f\n", lat, lon) 037 fmt.Printf("Fuzz: %f,%f\n", 038 latFuzz, lonFuzz) 039 patch(img, latFuzz, lonFuzz) 040 } 041 042 func patch(path string, 043 lat, lon float64) { 044 var out bytes.Buffer 045 cmd := exec.Command( 046 "exiftool", path, 047 fmt.Sprintf("-gpslatitude=%f", lat), 048 fmt.Sprintf("-gpslongitude=%f", lon)) 049 cmd.Stdout = &out 050 cmd.Stderr = &out 051 052 err := cmd.Run() 053 if err != nil { 054 panic(out.String()) 055 } 056 } 057 058 func geopos(path string) ( 059 float64, float64, error) { 060 f, err := os.Open(path) 061 if err != nil { 062 return 0, 0, err 063 } 064 065 x, err := exif.Decode(f) 066 if err != nil { 067 return 0, 0, err 068 } 069 070 lat, lon, err := x.LatLong() 071 if err != nil { 072 return 0, 0, err 073 } 074 075 return lat, lon, nil 076 } 077 078 func fuzz(lat, lon float64) ( 079 float64, float64) { 080 r := 1000.0 / 111300 // 1km radius 081 082 // secret center 083 lat += .045 084 lon += .021 085 086 s1 := rand.NewSource( // random seed 087 time.Now().UnixNano()) 088 r1 := rand.New(s1) 089 090 u := r1.Float64() 091 v := r1.Float64() 092 093 w := r * math.Sqrt(u) 094 t := 2.0 * math.Pi * v 095 x := w * math.Cos(t) 096 y := w * math.Sin(t) 097 098 x = x / math.Cos(lat*math.Pi/180.0) 099 return lat + x, lon + y 100 }
Listing 2
Invoking the Fuzzer
$ geofuzz ebay.jpg Was: 37.756795,-122.426903 Fuzz: 37.804414,-122.407682
Look at the numbers in the output: My home in San Francisco is located at longitude 37° north and latitude 122° west, so the value for 37 is positive and 122 is negative. For comparison: Munich's Marienplatz is located at the geocoordinates 48.137365 and 11.575127, which can easily be retrieved in Google Maps by right-clicking the mouse on the corresponding location and selecting What's here? in the context menu (Figure 4). This confirms that Munich is further north than San Francisco and not west of the prime meridian (zero degree longitude), but east. Therefore, the value for Munich's 11° longitude is positive.
Reading Is Easier than Writing
If the number of arguments passed on the command line is less than expected, line 24 in Listing 1 branches to the usage()
function that starts in line 15, which displays the error, demonstrates the correct use, and terminates the program with an exit code of 1
.
The geodata are available as latitude and longitude in degrees, minutes, and seconds in the Exif tags of the photo file's JPEG format. The go-exif2 library on GitHub makes it surprisingly easy to read this relatively complex structure [1]. Luckily, I remembered that I had used the library once before in this magazine in an application for geosearching in a photo collection [3].
The geopos()
function in line 58 of Listing 1 opens the image file passed to it by name, decodes the JPEG format with the call of the library function Decode()
, and finds the latitude and longitude information of the location stored in the Exif tags with LatLong()
. The function returns both values as floating-point numbers to the main program. This in turn calls the fuzz()
function with them in line 34, which puts on the obfuscation filter (in line 78).
Math in Space
If you travel any distance on the surface of the earth, you are not, strictly speaking, moving in a two-dimensional space, but on the surface of a more or less even sphere. The distance travelled from one place to another, which are given as latitude and longitude, therefore cannot be computed by using simple two-dimensional Euclidean geometry, but it has to take into account the third dimension on the great circle of the sphere.
The fuzzer therefore has to calculate the distance (x
, y
) from the latitude and longitude of a starting point (x0
, y0
) from which someone moves away in a random direction on the great circle within the radius r
. Fortunately, an expert on stack overflow has already found the solution to this geometric puzzle [4].
The radius r
of the circle within which the algorithm scatters the coordinates is given in meters and not in degrees. To convert, line 80 divides the value of 1,000 meters (which corresponds to a scattering circle with a radius of one kilometer) by 111,300. Where does this constant come from? It corresponds to the distance in meters travelled by someone on the equator who moves exactly one degree. Since the earth has a circumference of about 40,075 kilometers at that point, one degree corresponds to the 360th part of it (i.e., about 111,300 meters).
As far as scattering random points is concerned, it helps to first simplify the assumption that the algorithm places the target points in a two-dimensional circle with the radius r
. With two randomly generated values u
and v
in the range of [0,1[
, Listing 3 gives the polar coordinates of the move, which can be converted into Cartesian coordinates x
and y
with Listing 4.
Listing 3
Polar Coordinates
01 w = r * sqrt(u) 02 t = 2 * Pi * v
Listing 4
Cartesian Coordinates
01 x = w * cos(t) 02 y = w * sin(t)
Attentive readers may be wondering about the root sqrt(u)
in the first line in Listing 3 – why doesn't the moving vector length w
simply result from r * u
, creating values evenly between zero and r
? This is because if the radii w
were distributed linearly between zero and r
, the random points would not be distributed evenly on the circular surface. If half of the points were below r/2
, half of the results would be concentrated on the inner circle area, which contains only a quarter of the entire circular area. The root function corrects this and distributes the points evenly over the entire circular area.
However, the algorithm now has to take into account the fact that the circular surface is not on a two-dimensional plane, but on the globe. On the surface of the earth, radial distances given in degrees at the equator are at scale, but as the globe gets narrower toward the poles, the same segments of a circle get shorter in the west-east direction. After all, a degree in latitude at the equator is a longer distance than a degree further up or down and shrinks to zero near the poles. A correction formula extends the degree values for the calculated circle's x-direction (i.e., the determined difference in longitude) for regions further away from the equator:
x' = x / cos(y0)
Lastly, the latitude y0
is given in degrees and not as a radian, but the implementation of the cosine function in many programming languages expects radian values. Therefore, fuzz()
converts the degrees into radians before applying the correctional east-west expander:
x = x / math.Cos(y*math.Pi/180.0)
Keep in mind that this method provides only an approximation, but it works well enough for relatively small circles and far away from the polar regions. Truth be told, since we're only dealing with random placements, a simpler approach to the fuzzing problem at hand would also have been possible, but hopefully this excursion shed some light on the fascinating field of globe geometry.
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.
-
New KDE Slimbook Plasma Available for Preorder
Powered by an AMD Ryzen CPU, the latest KDE Slimbook laptop is powerful enough for local AI tasks.